マップ型は、キーの集合をループして新しいオブジェクト型を作ります。{ [K in keyof T]: boolean } は「T の各キー K について、boolean 型の K という名前のプロパティ」という意味です:
Features にキーを追加すれば、Flags<Features> もそれを要求します。マップ型はコンパイル時だけのものです。オブジェクトを表すだけで、作りはしません。
構文
マップ型には3つの部分があります。キーの変数、反復するキーのユニオン型、そしてキーの変数を使ってもよいプロパティの型です。
type MappedType = {
[K in Keys]: PropertyType; // K takes each member of Keys in turn
};
Keys は文字列、数値、シンボルのどんなユニオン型でもよく、keyof から来る必要はありません:
これはまさに Record<Size, number> がすることで、Record は1行のマップ型です。プロパティの型ではキーを使えます。{ [K in keyof T]: T[K] } はインデックスアクセスで各プロパティの型をそのままコピーし、{ [K in keyof T]: T[K] | null } はすべてのプロパティを null 許容にします。
修飾子: +/- を使った readonly と ?
マップ型は、すべてのプロパティの readonly と省略可能(?)の修飾子を一度に追加したり取り除いたりできます。追加するには +(何も書かなければこれがデフォルト)、取り除くには - を前に付けます:
最後の行は、readonly がコンパイル時だけのルールであることを示しています。代入は報告されました(ここでは @ts-expect-error で抑えています)が、出力されたJavaScriptはそれでも実行しました。-? は疑問符を外すだけではありません。プロパティの型から undefined も取り除くので、AllRequired<Account> は { id: 1, email: undefined } を error TS2322: Type 'undefined' is not assignable to type 'string' で拒否します。
Partial、Readonly、Pick、Record の作り方
オブジェクトの形を変える組み込みのユーティリティ型は、マップ型です。次はTypeScript自身の lib.es5.d.ts にある定義です:
type Partial<T> = { [P in keyof T]?: T[P] };
type Required<T> = { [P in keyof T]-?: T[P] };
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Record<K extends keyof any, T> = { [P in K]: T };
これを読むのはよい練習になります。Pick は渡したキーだけを反復し、Record は元の型を無視してすべてのキーに同じ値の型を与えます。Omit は独自のマップ型ではなく、Pick<T, Exclude<keyof T, K>> です。すべての一覧は ユーティリティ型のリファレンス にあります。
as によるキーの再マッピング
キーのあとの as 句はプロパティ名を変えます。テンプレートリテラル型と組み合わせると、古い名前から新しい名前を生成できます:
string & K があるのは、keyof T には number や symbol のキーが含まれうるのに、Capitalize は文字列しか受け付けないからです。交差型が文字列のキーを残し、それ以外を捨てます。
never でキーを絞り込む
as 句があるキーに対して never を返すと、そのキーは取り除かれます。条件型と組み合わせれば、プロパティをその型に応じて残したり捨てたりできます:
DataOnly のような型は、JSON.stringify を通って残るものや、フォームが編集するもの、つまりメソッドを除いたフィールドを表すのに便利です。
どの修飾子が保たれるか
ある型 T の keyof T に対するマップ型は準同型(homomorphic)と呼ばれ、T から各プロパティの readonly と ? の修飾子をコピーします。普通のキーのユニオン型に対するマップ型は、修飾子なしから始まります:
type Account = { readonly id: number; email?: string };
type Copy<T> = { [K in keyof T]: T[K] };
type A = Copy<Account>;
// { readonly id: number; email?: string } (modifiers kept)
type B = { [K in "id" | "email"]: Account[K] };
// { id: number; email: string | undefined } (email is required now)
Account["email"] が undefined を含むので、B の email の型にはまだ undefined がありますが、プロパティ自体はもう省略可能ではありません。{ id: 1 } は拒否されます。Partial<T> などが keyof T に対してマップするのはこのためで、明示的に変えないものはすべて保たれます。
ジェネリックな準同型のマップ型を配列やタプルに適用すると、数値のキーを持つオブジェクトではなく、配列やタプルが得られます。Readonly<string[]> は readonly string[] で、type Box<T> = { [K in keyof T]: { value: T[K] } } なら Box<[string, number]> は [{ value: string }, { value: number }] です。
マップ型と実行時のコード
マップ型はオブジェクトを表すだけなので、そうしたオブジェクトを作る関数には独自の実行時のループが必要です。型はその結果を表します:
コールバックは、型注釈なしでマップ型から引数の型を得ます(一方では v は string、もう一方では number)。型のマッピングではなく Map クラスを探してここに来たなら、TypeScriptのMap を参照してください。
よくある質問
TypeScriptのマップ型とは何ですか?
キーのユニオン型を反復してオブジェクト型を作る型です: { [K in Keys]: SomeType }。キーはたいてい keyof T なので、新しい型は T と同じキーを持ち、プロパティの型が変換されます。type Flags<T> = { [K in keyof T]: boolean } のような形です。
マップ型の +readonly、-readonly、+?、-? はどういう意味ですか?
修飾子を追加したり取り除いたりします。readonly または +readonly はすべてのプロパティを readonly にし、-readonly は readonly を取り除きます。? または +? はすべてのプロパティを省略可能にし、-? は必須にするとともに型から undefined も取り除きます。Required<T> は -? で書かれています。
マップ型でキーの名前を変えるには?
キーのあとに as 句を使います: { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] } は name を getName に変えます。as 句でキーを never にするとそのキーは取り除かれ、これがプロパティを絞り込む方法です。
マップ型は TypeScriptの Map と同じですか?
違います。マップ型はコンパイル時の型の変換で、コードを生成しません。Map<K, V> はJavaScriptの Map クラスで、キーと値のペアを持つ実行時のコレクションです。「typescript map 型」という検索は、どちらかの意味で使われていることがよくあります。
Partial と Readonly はどのように実装されていますか?
標準ライブラリのマップ型として実装されています: type Partial<T> = { [P in keyof T]?: T[P] } と type Readonly<T> = { readonly [P in keyof T]: T[P] }。Pick と Record もマップ型で、Omit は Pick と Exclude を組み合わせたものです。