Record<K, V> は、キーが K 型で値がすべて V 型のオブジェクトを表す組み込みのユーティリティ型です。string のキーなら辞書を表し、リテラルのキーのユニオン型なら、ちょうどそのキーを持たなければならないオブジェクトを表します。
Record は型システムの中にしか存在しません。実行時にはどちらのオブジェクトも普通のJavaScriptのオブジェクトなので、オブジェクトリテラル、スプレッド、JSON.stringify、そのほかオブジェクトを受け取るあらゆるものと一緒に使えます。
構文と定義
Record<Keys, Value>
Keys はオブジェクトのキーになれるものでなければなりません。string、number、symbol、文字列や数値のリテラルのユニオン型、またはテンプレートリテラル型です。Value はどんな型でもかまいません。TypeScriptの標準ライブラリでの定義は、1行のマップ型です:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
keyof any は string | number | symbol で、ありうるすべてのキーの型の集合です。[P in K]: T は、K の各メンバーについて T 型のプロパティを1つ作ります。これで下の2つの動きが説明できます。string のような広い K はインデックスシグネチャ(任意のキー)を作り、ユニオン型の K はメンバーごとに1つの必須プロパティを作ります。
ユニオン型のキー: すべてのキーが必須
キーがリテラルのユニオン型のとき、Record はそのすべてを並べなければならず、それ以外は書けません。コンパイラーがチェックリストになります:
Status に "cancelled" を追加すると、新しい状態にラベルと色を与えるまで、両方のオブジェクトがコンパイルできなくなります。キーを省いたり、ユニオン型にないキーを追加したりするとコンパイルエラーです:
index.ts(4,7): error TS2741: Property 'error' is missing in type '{ idle: string; loading: string; success: string; }' but required in type 'Record<Status, string>'.
index.ts(11,54): error TS2353: Object literal may only specify known properties, and 'paused' does not exist in type 'Record<Status, string>'.
キーの型として文字列 enum を使っても同じです。Record<Color, string> は enum のメンバーごとに1つのエントリーを要求します。
Record<string, T> と存在しないキー
string のキーではどんなキーも許され、TypeScriptは存在しないキーであってもすべての読み取りを T と型付けします。実行時には存在しないキーは undefined になります:
これが Record でいちばんよくあるバグです。対処法は3つあります。読む前に in か Object.hasOwn でチェックする、値を V | undefined と宣言する、またはコンパイラーのオプション noUncheckedIndexedAccess を有効にして、プロジェクト内のすべてのインデックスシグネチャの読み取りに | undefined を加える、です。ユニオン型のキーを持つ Record にはこの問題はありません。すべてのキーが存在することが保証されているからです。
Partial<Record<K, V>>: 一部のキーだけ
キーのユニオン型を使いつつ、すべてを必須にしたくないなら、Record を Partial で囲みます。読み取りは V | undefined を返し、これが正直な型です:
jp のようにキーを打ち間違えるとやはりエラーになります。これが Record<string, string> に対する利点です。
Record をループする
Object.keys、Object.values、Object.entries はどれも使えます。落とし穴はキーの型です。Object.keys は string[] を、Object.entries は [string, V][] を返し、キーのユニオン型にはなりません:
TypeScriptがキーを意図的に string のままにしているのは、オブジェクトが実行時に型に書かれた以上のプロパティを持ちうるので、一般には Plan[] を約束するのは安全ではないからです。seats のようにリテラルから作ったオブジェクトなら、キャストしても安全です。
データから Record を作る
Record は、配列をグループ化したりインデックス化したりした結果としてよく使われます。Record 型の空のオブジェクトから始めて埋めていきます:
Book["genre"] はインターフェースのユニオン型をキーの型として再利用しているので、Book にジャンルを追加すると byGenre が新しいエントリーを要求します。
Record<string, unknown> とインターフェース
Record<string, unknown> は「文字列のキーを持つ何かのオブジェクト」によく使われる型です。オブジェクトリテラルや type エイリアスで型付けした値は受け付けますが、インターフェースは拒否されます:
index.ts(11,11): error TS2345: Argument of type 'User' is not assignable to parameter of type 'Record<string, unknown>'.
Index signature for type 'string' is missing in type 'User'.
インターフェースは宣言のマージで拡張できるので、TypeScriptはインターフェースがインデックスシグネチャに当てはまるとは考えません。型エイリアスはあとから開き直せないので、type User = { name: string } なら通ります。よくある直し方は、代わりに object を受け付ける(それでも Object.keys は呼べます)、関数をジェネリックにする(<T extends object>(obj: T))、User を type で宣言する、のいずれかです(ほかの違いは interface と type の違い のページにあります)。
Record、インデックスシグネチャ、Map の比較
Record<K, V> | { [key: string]: V } | Map<K, V> | |
|---|---|---|---|
| 実行時に存在するか | いいえ、普通のオブジェクト | いいえ、普通のオブジェクト | はい、クラス |
| キーの集合を固定できるか | はい、ユニオン型の K で | いいえ | いいえ |
| キーの型 | string、number、symbol、リテラルのユニオン型、テンプレートのパターン | string、number、symbol、テンプレートのパターン | 何でも、オブジェクトも |
| 存在しないキーの読み取りの型 | V(string のキーの場合) | V | get から V | undefined |
| 名前付きのプロパティとの混在 | 交差型 & で | はい、同じ型の中で | いいえ |
| JSONとスプレッド | 使える | 使える | 使えない、先に変換が必要 |
| サイズ | Object.keys(r).length | Object.keys(o).length | m.size |
| 頻繁な追加と削除 | 動く | 動く | そのために設計されている |
キーの集合がわかっているときは、ユニオン型のキーを持つ Record を選びましょう。すべてのキーが存在するかをチェックできる唯一の方法です。制限のない文字列のキーなら、Record<string, V> とインデックスシグネチャは交換可能で、読みやすさから Record を好むコードベースが多くあります。実行時にキーを追加・削除する場合、キーが文字列でない場合、余計な手間なしにサイズと挿入順が必要な場合は Map を使いましょう。
よくある質問
TypeScriptの Record とは何ですか?
Record<K, V> は、キーが K 型で値がすべて V 型のオブジェクトを表す組み込みのユーティリティ型です。Record<string, number> は任意の文字列のキーと数値の値を持つオブジェクト、Record<"en" | "de", string> はちょうど en と de のキーを持ち、どちらも文字列であるオブジェクトです。
TypeScriptの Record と Map の違いは何ですか?
Record は普通のJavaScriptのオブジェクトの型なので、オブジェクトリテラル、JSON、スプレッドと一緒に使え、コンパイル時に消えます。Map は get、set、has、size を持つ実行時のクラスで、すべてのキーの挿入順を保ち、どんな型のキー(オブジェクトも)でも受け付け、get は V | undefined を返します。固定のデータやJSONの形のデータには Record を、実行時にキーを追加・削除するなら Map を使いましょう。
Record<string, T> と { [key: string]: T } の違いは何ですか?
値としては同じ型です。Record<string, T> は文字列のインデックスシグネチャを持つオブジェクト型に展開されます。小さな違いが2つあります。インデックスシグネチャには名前を付けられ、同じ型の中でほかのプロパティと並べられます。また、keyof Record<string, T> は string ですが、keyof { [key: string]: T } は string | number です。
TypeScriptで Record をループするには?
キーと値のペアには Object.entries(record)、キーには Object.keys、値には Object.values を使います。オブジェクトは実行時に余分なキーを持ちうるので、キーは K ではなく string として返ります。Record が既知のキーのユニオン型を持つなら、キャストします: (Object.keys(r) as Array<keyof typeof r>)。
Record の一部のキーだけを必須にするには?
ユニオン型のキーでは、Record<K, V> はすべてのキーを要求します。すべてを省略可能にするには Partial で囲みます: Partial<Record<Lang, string>>。混在させるには交差型を使います: Record<"en", string> & Partial<Record<"de" | "fr", string>> は en を必須にし、ほかを任意にします。