メインコンテンツまでスキップ

Record<Keys, Type>

Record<Keys, Type>はプロパティのキーがKeysであり、プロパティの値がTypeであるオブジェクトの型を作るユーティリティ型です。

Record<Keys, Type>の型引数

Keys

オブジェクトのプロパティーキーを指定します。Keysに代入できる型は、stringnumbersymbolとそれぞれのリテラル型です。

Type

オブジェクトのプロパティの値の型を指定します。任意の型が代入できます。

Recordの使用例

キーがstringで値がnumberのインデックス型を定義する。

ts
type StringNumber = Record<string, number>;
const value: StringNumber = { a: 1, b: 2, c: 3 };
ts
type StringNumber = Record<string, number>;
const value: StringNumber = { a: 1, b: 2, c: 3 };

キーがfirstNamemiddleNamefamilyNameで、値が文字列になるオブジェクトの型を定義する。

ts
type Person = Record<"firstName" | "middleName" | "lastName", string>;
const person: Person = {
firstName: "Robert",
middleName: "Cecil",
lastName: "Martin",
};
ts
type Person = Record<"firstName" | "middleName" | "lastName", string>;
const person: Person = {
firstName: "Robert",
middleName: "Cecil",
lastName: "Martin",
};

インデックスアクセスの注意点

Record<string, ...>のようにキーにstringなど、リテラル型でない型を指定した場合は、インデックスアクセスに注意してください。存在しないキーにアクセスしても、キーが必ずあるかのようにあつかわれるためです。

次の例のように、Record<string, number>型のdictオブジェクトには、aキーはあるのに対し、bキーはありません。しかし、dict.bnumberとして推論されます。

ts
const dict: Record<string, number> = { a: 1 };
dict.b;
number
ts
const dict: Record<string, number> = { a: 1 };
dict.b;
number

実際のdict.bの値はundefinedになるので、もしもdict.bのメソッドを呼び出すと実行時エラーになります。

ts
const dict: Record<string, number> = { a: 1 };
console.log(dict.b);
undefined
dict.b.toFixed(); // 実行時エラーが発生する
ts
const dict: Record<string, number> = { a: 1 };
console.log(dict.b);
undefined
dict.b.toFixed(); // 実行時エラーが発生する

このような挙動は、型チェックで実行時エラーを減らしたいと考える開発者にとっては不都合です。

この問題に対処するため、TypeScriptにはコンパイラオプションnoUncheckedIndexedAccessが用意されています。これを有効にすると、インデックスアクセスの結果の型がT | undefinedになります。つまり、undefinedの可能性を考慮した型になるわけです。そのため、dict.bのメソッドを呼び出すコードはコンパイルエラーになり、型チェックの恩恵が得られます。

ts
const dict: Record<string, number> = { a: 1 };
dict.b;
number | undefined
dict.b.toFixed();
'dict.b' is possibly 'undefined'.18048'dict.b' is possibly 'undefined'.
ts
const dict: Record<string, number> = { a: 1 };
dict.b;
number | undefined
dict.b.toFixed();
'dict.b' is possibly 'undefined'.18048'dict.b' is possibly 'undefined'.

📄️ noUncheckedIndexedAccess

インデックス型のプロパティや配列要素を参照したときundefinedのチェックを必須にする

一方、Recordのキーが"firstName" | "lastName"のようなリテラル型だけで構成される場合は、noUncheckedIndexedAccessの設定にかかわらず、この問題は発生しません。キーが限定されているため、存在しないキーへのアクセスはコンパイルエラーになるからです。

ts
// noUncheckedIndexedAccessがfalseの場合
type Person = Record<"firstName" | "lastName", string>;
const person: Person = {
firstName: "Robert",
lastName: "Martin",
};
const firstName = person.firstName;
const firstName: string
person.b; // 存在しないキーへのアクセス
Property 'b' does not exist on type 'Person'.2339Property 'b' does not exist on type 'Person'.
ts
// noUncheckedIndexedAccessがfalseの場合
type Person = Record<"firstName" | "lastName", string>;
const person: Person = {
firstName: "Robert",
lastName: "Martin",
};
const firstName = person.firstName;
const firstName: string
person.b; // 存在しないキーへのアクセス
Property 'b' does not exist on type 'Person'.2339Property 'b' does not exist on type 'Person'.

キーがstringのときはnoUncheckedIndexedAccessを有効にすると、コンパイラーはundefinedを含めるようになりますが、キーがリテラル型(またはリテラル型のユニオン)のときは、コンパイラーはundefinedを含めないようになります。キーが必ずあることが、リテラル型によるキー指定によって自明だからです。

ts
// noUncheckedIndexedAccessがtrueの場合
type Person = Record<"firstName" | "lastName", string>;
const person: Person = {
firstName: "Robert",
lastName: "Martin",
};
const firstName = person.firstName; // undefinedは含まれない
const firstName: string
ts
// noUncheckedIndexedAccessがtrueの場合
type Person = Record<"firstName" | "lastName", string>;
const person: Person = {
firstName: "Robert",
lastName: "Martin",
};
const firstName = person.firstName; // undefinedは含まれない
const firstName: string

関連情報

📄️ インデックス型

TypeScriptで、オブジェクトのフィールド名をあえて指定せず、プロパティのみを指定したい場合があります。そのときに使えるのがこのインデックス型(index signature)です。たとえば、プロパティがすべてnumber型であるオブジェクトは次のように型注釈します。

📄️ Mapped Types

インデックス型では設定時はどのようなキーも自由に設定できてしまい、アクセス時は毎回undefinedかどうかの型チェックが必要です。入力の形式が決まっているのであればMapped Typesの使用を検討できます。

📄️ Map<K, V>

MapはJavaScriptの組み込みAPIのひとつで、キーと値のペアを取り扱うためのオブジェクトです。Mapにはひとつのキーについてはひとつの値のみを格納できます。