Menu

TypeScriptのMapped Types: 構文、修飾子、キーの再マッピング

マップ型は、キーをループして新しいオブジェクト型を作ります: { [K in keyof T]: ... }。構文、+/- を使った readonly と ? の修飾子、as によるキーの再マッピング、キーの絞り込み、そして Partial、Readonly、Required、Pick、Record がどう書かれているかを解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

マップ型は、キーの集合をループして新しいオブジェクト型を作ります。{ [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 を組み合わせたものです。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める