41
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TypeScript の Key Remapping 使ってる? in と as でオブジェクトのキーを自由自在に。

Last updated at Posted at 2022-01-06

概要

TypeScript 4.1 から Key Remapping という型の記述方法があり業務で実際に使ってみたら便利だったのですが、日本語の情報が少ないように見えるのでまとめておきます。

Key Remapping は、 Mapped Types で as を使うことで、キーの型を任意の型に上書きする記法です。
オブジェクトの型を定義する時に in を使うことがあると思いますが、 Key Remapping はこの in と共に as を使用します。
(この as は Type Assertion の as とは別ものです)

使い方

まず単純な Mapped Types の例を用意します。


type Key = "hoge" | "piyo";
type Obj = { [P in Key]: string };

// Obj は下記のような型になる。

type Obj = {
  hoge: string;
  piyo: string;
}

これを、 Key Remapping を使用して上書きするとこんな感じ:arrow_down:


type Key = "hoge" | "piyo";
type Obj = { [P in Key as `remapped-${P}`]: string };

// Obj は下記のような型になる。

type Obj = {
  "remapped-hoge": string;
  "remapped-piyo": string;
}


ポイントは in の後に as を使って上書きしたい型を指定しているところです。
Mapped Types なのでもちろん現在のキーの型(上の例でいう P)を使用できるので色々できます。

{ [P in Key as 任意の上書きしたい型]: ~~~ };

上記の例では Template Literal Type を使っていますが、指定する型はキーの型である string | number | symbol を満たしていればなんでも大丈夫です。

type Key = "hoge" | "piyo";
type Obj = { [P in Key as "FILTERED"]: string };

// Obj は下記のような型になる。

type Obj = {
  "FILTERED": string;
}

string | number | symbol 以外を渡すとコンパイルエラーです。

never を返すとキーを除外する

そして Key Remapping のもう一つ重要な機能は、 asnever を指定するとそのキーを結果の型から除外してくれる機能です。

type Key = "hoge" | "piyo";
type Obj = { [P in Key as never]: string };

// Obj は下記のような型になる。

type Obj = {}

記法としてはこれだけで単純なんですが、どういう場面で使うのかを書いていきます。

使用例

一定規則に従ってキーをリネームする

// 公式ドキュメントより引用。

type Getters<Type> = {
    [Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};
 
interface Person {
    name: string;
    age: number;
    location: string;
}
 
type LazyPerson = Getters<Person>;

// LazyPerson は下記のような型になる。

type LazyPerson = {
    getName: () => string;
    getAge: () => number;
    getLocation: () => string;
}

この例の場合は、全てのキーの prefix として get をつけることで getter 関数を定義しています。

[Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]

Capitalize...TypeScriptの型はもうなんでもありだ

あるオブジェクトの特定の値をキーとした別オブジェクトを定義する

日本語で表現したら難しくなった。

// 公式ドキュメントより引用

type EventConfig<Events extends { kind: string }> = {
    [E in Events as E["kind"]]: (event: E) => void;
}

// kind だけ共通の異なる型
type SquareEvent = { kind: "square", x: number, y: number };
type CircleEvent = { kind: "circle", radius: number };
 
type Config = EventConfig<SquareEvent | CircleEvent>

// Config は下記のような型になる

type Config = {
    square: (event: SquareEvent) => void;
    circle: (event: CircleEvent) => void;
}

コード見ると確かにこういう用途ありそうな気がしてくる。

けどこの部分:arrow_down:トリッキーですね。

[E in Events as E["kind"]]: (event: E) => void;

これ:arrow_down:コンパイルエラーだし...

type EventConfig<Events extends { kind: string }> = {
  // 型 '{ kind: string; }' を型 'symbol' に割り当てることはできません。
  [E in Events]: (event: E) => void;
}

既存のプロパティに別名のプロパティを定義する

type Original = {
  hoge: string;
  piyo: number;
};

type HasAlias = { [P in keyof Original as P | `${P}Alias`]: Original[P] };

// HasAlias は下記のような型になる。

type HasAlias = {
  hoge: string;
  hogeAlias: string;
  piyo: number;
  piyoAlias: number;
};

ポイントは、 as で指定している型が Union Type であることですね。
Union Type の場合、列挙されたリテラルのキーを全て追加します。

特定のキーを除外する

never を返すとキーを除外できる機能を使います。

// 公式ドキュメントより引用

type RemoveKindField<Type> = {
    [Property in keyof Type as Exclude<Property, "kind">]: Type[Property]
};
 
interface Circle {
    kind: "circle";
    radius: number;
}
 
type KindlessCircle = RemoveKindField<Circle>;

// KindlessCircle は下記のような型になる。

type KindlessCircle = {
    radius: number;
}

ポイントは asExclude を指定しているところです。
Exclude は特定の型から指定した型に代入可能な型のみを除外する型ですが、 Exclude<"HOGE", "HOGE"> は(当てはまるものが一つもないので) never を返します。
上記の例の場合、"kind" に当てはまれば never を返す、つまりキーを除外します。

a という文字を含まないキーを除外する


type Fruits = {
  banana: string;
  grape: number;
  apple: { color: string };
  cherry: string;
  kiwi: number;
  lemon: symbol;
};

type FruitsIncludingA = { [P in keyof Fruits as Extract<P, `${string}a${string}`> ]: Fruits[P] };

// FruitsIncludingA は下記のような型になる。

type FruitsIncludingA = {
    banana: string;
    grape: number;
    apple: {
        color: string;
    };
}

一個前の例と同様、ポイントは asExtract を指定しているところです。
Extract は 特定の型から指定した型に代入可能な型のみを抽出する型ですが、 Extract<"HOGE", "PIYO"> は(当てはまるものが一つもないので) never を返します。
上記の例の場合、${string}a${string} に当てはまらなければ never を返す、つまりキーを除外します。

フルーツの名前の中で a を含んでいるものはどれだろう?と思った時に役立ちます。

値の型が〇〇であるキーを除外する

単に文字列を指定して除外とかは Omit 使えば良いんですが、これは Omit では実現できない使い方。
例えば**「このオブジェクトのうち、値が string 型であるキーだけを抽出したオブジェクト型を作りたい」**みたいな場合に使えます。

type Original = {
  hoge: string;
  piyo: number;
  fuga: string;
};
type RemappedObj = { [P in keyof Original as Original[P] extends string ? P : never]: Original[P] };

// RemappedObj は下記のような型になる。
// 値が number 型である piyo だけ除外されてる。

type RemappedObj = {
  hoge: string;
  fuga: string;
};

ただし、重複になりますがキーの文字列が固定でわかっている、もしくは外部から( keyof などを使って)動的に参照できる場合は Omit を使う方が良いと思います。それらが実現できない場面で重宝します。

ちなみに、Key Remapping を使わずに値側で never を返すと、型からキーが除外されません:arrow_down:

type Original = {
  hoge: string;
  piyo: number;
  fuga: string;
};
type RemappedObj = { [P in keyof Original]: Original[P] extends string ? Original[P] : never };

// RemappedObj は下記のような型になる。
// piyo は消えずに never 型のプロパティになる。

type RemappedObj = {
  hoge: string;
  piyo: never;
  fuga: string;
};

as で指定している Conditional Type の部分はもちろんもっと複雑なこともできるので、 string と関数だけとか他にも色々できると思います。

おしまい

Pick や Omit でも似たようなことはできますが、値を絡めてより複雑なことをしようとした時など、Key Remapping の方が適している場合もあります。
(というか Pick も Omit も定義は Mapped Types を使っているので、やっていることは根本的に同じようなものなのですが)

覚えておくと、どこかで役に立つかもしれません!

41
26
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?