概要
TypeScript 4.1 から Key Remapping という型の記述方法があり業務で実際に使ってみたら便利だったのですが、日本語の情報が少ないように見えるのでまとめておきます。
Key Remapping は、 Mapped Types で as
を使うことで、キーの型を任意の型に上書きする記法です。
オブジェクトの型を定義する時に in
を使うことがあると思いますが、 Key Remapping はこの in
と共に as
を使用します。
(この as
は Type Assertion の as
とは別ものです)
- 公式ドキュメント
- 公開時のアナウンス
- 追加された際のPR
- Template literal types and mapped type 'as' clauses by ahejlsberg · Pull Request #40336 · microsoft/TypeScript
- こちらを見てもわかる通り、 Template Literal Type と同時に追加されていて、併用することも意識されています。
使い方
まず単純な Mapped Types の例を用意します。
type Key = "hoge" | "piyo";
type Obj = { [P in Key]: string };
// Obj は下記のような型になる。
type Obj = {
hoge: string;
piyo: string;
}
これを、 Key Remapping を使用して上書きするとこんな感じ
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 のもう一つ重要な機能は、 as
で never
を指定するとそのキーを結果の型から除外してくれる機能です。
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;
}
コード見ると確かにこういう用途ありそうな気がしてくる。
けどこの部分トリッキーですね。
[E in Events as E["kind"]]: (event: E) => void;
これコンパイルエラーだし...
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;
}
ポイントは as
で Exclude
を指定しているところです。
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;
};
}
一個前の例と同様、ポイントは as
で Extract
を指定しているところです。
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 を返すと、型からキーが除外されません
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 を使っているので、やっていることは根本的に同じようなものなのですが)
覚えておくと、どこかで役に立つかもしれません!