TypeScriptのRecord型を使えば、キーと値の型をまとめて定義しながらキーの網羅までコンパイラに任せられますが、{ [key: string]: T } で書くのとどう違うのか迷う場面は多いものです。

オブジェクトの型を { [key: string]: number } で書いてきたけど、Record型に置き換えるべき?違いがよく分からない。
キーを全部そろえたか型でチェックしたいのに、書き方を間違えると網羅チェックが効かない。どう書けばいい?
Recordで定義したのに、値にアクセスしたらundefinedが返ってきてバグになった。なぜ?

この記事を読めば、Record型でキーを有限集合として網羅し、型安全なオブジェクトを設計できるようになります。インデックスシグネチャ(index signature)やobject型とのはっきりした使い分けに加え、多くの人がつまずくundefinedの罠まで実コードで確認していきます。

この記事は次のような方におすすめです。

この記事はこんな人におすすめ!
  • オブジェクトの型を { [key: string]: T } で書いてきたが、Record型に切り替えるべきか迷っている方
  • キーの取りこぼしをコンパイラに検出させ、網羅を型で強制したい方
  • Recordで安全だと思った値アクセスがundefinedになる理由を知りたい方
  • Record・インデックスシグネチャ・object型の使い分けを判断軸として持ちたい方

読み終えるころには、目の前のオブジェクト定義をRecordで書くべきか、インデックスシグネチャや明示的なobject型に任せるべきかを、根拠を持って選べるようになります。

それでは、順を追って詳しく見ていきましょう!

Record型とは|キーと値の型をまとめて定義するユーティリティ型

Record型は、指定したキーの集合と値の型をまとめて1つのオブジェクト型に展開するユーティリティ型です。Record<Keys, Type> と書くと、Keys に渡した名前を各プロパティ名に、Type をそのすべての値の型に割り当てたオブジェクト型ができあがります。

最小の例で確認してみましょう。

type Scores = Record<'math' | 'eng', number>;

// 上は次のオブジェクト型と等価
// type Scores = { math: number; eng: number };

const result: Scores = { math: 80, eng: 95 }; // OK

'math' | 'eng' というリテラルユニオンをキーに渡したことで、matheng の2つのプロパティを持ち、どちらも値が number のオブジェクト型になりました。プロパティ名と値の型を別々に並べて書く代わりに、キーの集合と値の型を分けて一度に指定できるのがRecordの基本的な利点です。

この挙動はTypeScript公式が標準で用意しているもので、Handbookでも組み込みユーティリティ型として説明されています(TypeScript Handbook「Utility Types」Record<Keys, Type>)。

Recordの型引数(KeysとType)

Record の1つ目の型引数 Keys にはプロパティ名になる型を、2つ目の Type には値の型を渡します。Keys に使えるのは stringnumbersymbol、およびそれらのリテラルやユニオンで、Type には任意の型を置けます。

// 任意の文字列キー・値はnumber
type AnyNumberMap = Record<string, number>;
const m: AnyNumberMap = { a: 1, b: 2, xyz: 3 }; // どんな文字列キーでもOK

// 'a' | 'b' の2キーに固定・値はnumber
type AB = Record<'a' | 'b', number>;
const ab: AB = { a: 1, b: 2 }; // a と b の両方が必須

両者の違いはキーが無限か有限かにあります。Record<string, number> は任意の文字列キーを受け付ける一方、Record<'a' | 'b', number>ab の2つを必須プロパティとして要求します。値側の型は自由で、number のような単純な型だけでなく、インターフェースやユニオン型を渡せば、より複雑なマップ型も表現できます。

Record型の内部はMapped Typeでできている

Recordが標準でどう定義されているかを見ておくと、後で扱うキー網羅やキー追加不可の挙動が腑に落ちます。Recordの実体は、Mapped Typeで各キーに同じ値の型を割り当てるだけの型です。

type Record<K extends keyof any, T> = {
  [P in K]: T;
};

[P in K]: T は、K(キーの集合)に含まれる各メンバー P をプロパティ名にし、その値の型を一律 T にするという意味です。K に有限のユニオンを渡せば、そのすべてのキーが必須プロパティとして展開されるため、1つでも欠けると型エラーになります。これがRecordでキー網羅が効く理由であり、同時に「定義済みのキー集合に後から別のキーを足せない」という性質の出どころでもあります。

Record型の基本的な使い方とよくある実用例

取りうるキーがあらかじめ決まっている「マップ的なデータ」は、Recordで型付けすると過不足なく書けます。文字列リテラルユニオンをキーにすると、設定値やスコア表、権限マトリクスのように「キーの集合が決まっているデータ」を、キーと値の型を分けて定義できます。

設定値や権限マトリクスをまとめて型付けする

同じ型の値を、決まったキーの集合に対してまとめて持たせたいときがRecordの出番です。環境ごとのフラグやロールごとの権限のように、キーが決まっていて値の型が共通するデータが典型例です。

// 設定値:環境ごとのフラグ
type Env = 'development' | 'staging' | 'production';
const debugEnabled: Record<Env, boolean> = {
  development: true,
  staging: true,
  production: false,
};

// 権限マトリクス:ロールごとの許可フラグ
interface Permission {
  read: boolean;
  write: boolean;
}
type Role = 'admin' | 'editor' | 'viewer';
const permissions: Record<Role, Permission> = {
  admin: { read: true, write: true },
  editor: { read: true, write: true },
  viewer: { read: true, write: false },
};

ここで「キーを3つ並べるだけなら、{ development: boolean; staging: boolean; production: boolean } と直接書いても同じでは?」と感じるかもしれません。実際、キーをその場に書き切るだけならRecordでなくてもかまいません。Recordが効いてくるのは、キーの集合(EnvRole)を型として切り出して使い回すときです。キー集合を1か所で定義しておけば、同じ集合を複数のマップで共有でき、メンバーを増やしたときの定義漏れも型で検出できます。値の型に Permission のようなインターフェースやユニオン型を置けるため、キーの集合と値の構造を分けて管理できるのもRecordの強みです。

ユニオン型・リテラル型をキーにして網羅を強制する

キーの取りこぼしをコンパイラに検出させたいなら、リテラルユニオンをキーに渡すことが要点です。ユニオンの各メンバーが必須プロパティになるため、1つでも欠けると型エラーで止まります。

type Lang = 'ja' | 'en' | 'fr';

// fr が欠けている
const greetings: Record<Lang, string> = {
  ja: 'こんにちは',
  en: 'Hello',
}; // 例:TS2741 Property 'fr' is missing in type ... (TypeScriptバージョンにより文言が異なる場合あり)

このように欠けたキーをコンパイル時に指摘できるのが、リテラルユニオンキーの最大の価値です。一方で、網羅チェックが効くのはキーが有限のユニオンのときだけで、Record<string, string> のようにキーが無限の場合は「すべての文字列を埋めた」状態を表現できないため、網羅の強制はかかりません。網羅させたいときは必ず有限のリテラルユニオンをキーに使う、と覚えておくとよいですよ。

なかむぅ
なかむぅ
既存オブジェクトのプロパティ名からキーの型を取り出すkeyof typeofについては、こちらで解説しています。
TypeScriptのkeyofが分からない人向け|typeof併用まで完全理解TypeScriptのkeyofでオブジェクト型からキーの型を取り出す基本から、keyof typeofで値からキー型を作る書き方、ジェネリクスでの型安全なアクセス、any・配列・union型で挙動が変わる注意点までコード例で整理します。...

Enumや動的なキーと組み合わせる

キーをEnumで管理しているなら、EnumのメンバーをそのままユニオンキーとしてRecordに渡せます。Enum全体をキーに使うことで、メンバーが増えたときの定義漏れも型で防げます。

enum Direction {
  Up = 'UP',
  Down = 'DOWN',
}

const labels: Record<Direction, string> = {
  [Direction.Up]: '上',
  [Direction.Down]: '下',
};

これに対し、APIレスポンスを仮受けする場合などキーが実行時まで分からないケースでは、Record<string, T> のようにキーを文字列全般に開く書き方をします。

// 動的に決まるキーを仮受けする
const apiResult: Record<string, number> = {};
apiResult['userId_123'] = 10; // 任意の文字列キーを追加できる

ただし Record<string, T> は任意のキーを許す代わりに、存在しないキーへアクセスしても型上は値が存在することになり、実行時にundefinedが返りうる点に注意が必要です。動的なキーで辞書的にデータを持ち回す運用は、Recordだけでなく専用のデータ構造で扱うほうが向く場面もあります。

Record型とインデックスシグネチャの違い【比較表+判断フロー】

両者は「キー付きのオブジェクト」を表す点では同じですが、得意分野がはっきり分かれます。一文にすると、Recordは「キー集合が型として決まっているマップ」に強く、インデックスシグネチャは「任意キーでアクセスできる」という構造そのものの表現に強い——これが使い分けの軸です。

抽象的な観点よりも、実際の場面ごとにどちらを選ぶかで見たほうが早く決まります。

場面 推奨 理由
キーが固定のユニオン(ロール名・環境名など) Record 全キーが必須になり、網羅を型で強制できる
任意の文字列キーの辞書 どちらでもよい(手軽なのは Record<string, V> Record<string, V>{ [k: string]: V } はほぼ同じ型
既知のプロパティと任意キーを混ぜたい インデックスシグネチャ Recordは固定キーと任意キーを1つの型に同居できない
読み取り専用の辞書にしたい Readonly<Record<K, V>> キー全体をまとめて読み取り専用にできる
キーごとに値の型を変えたい固定構造 明示的なobject型 キーごとに別々の値の型を指定できる

迷ったときは、次の順で判断すれば十分です。

  • キーが有限で網羅を効かせたい → Record
  • ただの辞書(任意キー) → どちらでもよい。短く書ける Record<string, V> で十分
  • 既知キーと任意キーを混ぜたい/interfaceに添字アクセスを持たせたい → インデックスシグネチャ

インデックスシグネチャとの機能差(網羅チェックとキーの自由度)

Recordとインデックスシグネチャの具体的な差は、「キーを固定して網羅を強制するか」「任意キーを自由に許すか」というトレードオフに集約されます。同じ値の型でも、キーの扱いが正反対です。

// インデックスシグネチャ:任意キー可・網羅なし
type FlexMap = { [key: string]: number };
const f: FlexMap = { a: 1 }; // b が無くてもOK、未知キーも追加できる

// Record(有限キー):網羅あり・キー固定
type FixedMap = Record<'a' | 'b', number>;
const r: FixedMap = { a: 1 }; // 例:TS2741 Property 'b' is missing in type ... (バージョンにより文言が異なる場合あり)

インデックスシグネチャは柔軟さと引き換えに網羅チェックを失い、Recordは網羅を得る代わりにキー集合が固定されます。

注意したいのは、キーが完全に任意のときは Record<string, number>{ [key: string]: number } がほぼ同じ型になることです。この場合はどちらで書いても差はなく、むしろ有限キーで網羅したい場面ではRecordのほうが短く書けます。インデックスシグネチャにしかできないのは、いくつかの既知のプロパティと任意キーを1つの型に同居させる書き方です。

// id は必須、それ以外は任意の文字列キーを許す
type Config = {
  id: number;
  [key: string]: number;
};

ただし既知のプロパティを混ぜるときは、その既知のプロパティの値の型もインデックスシグネチャの値の型に収まる必要がある点に注意します。値の型が string のインデックスシグネチャに数値のプロパティを足すとエラーになるため、その場合は string | number のように値の型を広げて両方を受け入れる形にします。

「決まったキー+任意キー」を1つの型で表したいときは、Recordでは書けずインデックスシグネチャの出番です。逆に、こうした混在が不要でキーが有限なら、Recordのほうが短く書けて網羅も効くため、無理にインデックスシグネチャを選ぶ必要はありません。

Record型の落とし穴|undefinedとキー追加の注意点

Recordは型安全に見えて、踏みやすい罠が2つあります。1つはアクセス結果のundefined、もう1つはキー集合が固定されて後から足せないことです。

特に Record<string, T> のようにキーが無限のRecordでは、存在しないキーにアクセスしても型上は T が返ることになっています。

const dict: Record<string, number> = { a: 1 };
const value = dict['missing']; // 型は number だが、実行時は undefined
value.toFixed(2); // 例:実行時に「Cannot read properties of undefined」

型は number なのに実物は undefined、という食い違いが実行時バグの温床になります。これを型で防ぐのが、次に扱う noUncheckedIndexedAccess です。

もう1つの注意点は、有限キーのRecordはキー集合が固定され、後から別のキーを足せないことです。網羅を強制できる強みの裏返しで、すべてのキーを必須にしたうえで一部だけ任意にしたい場合は、Partialと組み合わせて任意化するパターンが使えます。

なかむぅ
なかむぅ
Recordの全プロパティを任意化するPartialとの併用については、こちらで解説しています。
TypeScriptのPartial|効かない罠とRequiredの書き分けTypeScriptのPartialで全プロパティを任意化する基本から、ネストに効かない罠・型安全の落とし穴・対になるRequiredとの書き分けまで実コードで解説。更新処理やフォームで迷わず使えるようになります。...

noUncheckedIndexedAccessでアクセスを安全にする

キーアクセスのundefinedを型で防ぐ鍵が、tsconfigの noUncheckedIndexedAccess を有効にすることです。これをオンにすると、インデックスアクセスの結果に自動で undefined が加わります。

// noUncheckedIndexedAccess: false(既定)
const a: Record<string, number> = {};
const x = a['key']; // 型: number

// noUncheckedIndexedAccess: true
const b: Record<string, number> = {};
const y = b['key']; // 型: number | undefined

strictNullChecks が有効な環境で noUncheckedIndexedAccess をオンにすると、y の型が number | undefined になり、未チェックのまま数値として使うコードをコンパイラが検出できます。安全に取り出すには、事前に存在チェックを挟みます。

const y = b['key']; // number | undefined
if (y !== undefined) {
  y.toFixed(2); // ここでは number に絞り込まれる
}

この設定の挙動は公式のtsconfigリファレンスにまとまっています(TypeScript tsconfig Reference「noUncheckedIndexedAccess」)。

よくある質問

Recordとインデックスシグネチャはどちらを使うべき?

キーがあらかじめ決まっていて取りこぼしを型で防ぎたいなら、Recordのほうが短く書けて網羅も効くため第一候補です。インデックスシグネチャが向くのは、キーが実行時まで決まらない場合と、いくつかの既知のプロパティと任意キーを1つの型に混在させたい場合(例:{ id: number; [key: string]: number })です。キーが完全に開いているだけなら Record<string, T> とインデックスシグネチャはほぼ同じなので、既知キーの有無で選べば迷いません。

Recordのキーに使える型は?

キーに使えるのは stringnumbersymbol と、それらのリテラルやユニオンです。オブジェクト型などはキーに渡せず、渡すと型制約に反するエラーになります(文言はバージョンにより異なる場合あり)。網羅を効かせたいときは有限のリテラルユニオンを使います。

似た名前の実行時コレクションとは何が違う?

Recordは型レベルでオブジェクトの形を表すユーティリティ型で、実行時の値ではありません。一方、似た役割の実行時コレクションは、メソッドを持つ実体のあるオブジェクトです。

【付録】さらに学びを深めるためのリソース


さらにTypescriptの学習を進めたい方のために、いくつかのリソースを紹介します。
これらのリソースを活用することで、TypeScriptの型システムについてより深い知識を得ることができるでしょう。

おすすめの書籍

ゼロからわかる TypeScript入門


技術評論社から出版されている「ゼロからわかる TypeScript入門」は、プログラミング初心者や本職プログラマーではない方を主な対象にした入門書です。

変数・条件分岐・ループといった基本から、クラスやインターフェース、モジュールまで段階的に学べる構成になっています。最終章ではWeb APIとJSONを使った非同期Webアプリの作成も体験できるので、「実際に動くものを作る」ところまで到達できます。

プロを目指す人のためのTypeScript入門


技術評論社の「プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで」、通称 ブルーベリー本 です。
JavaScriptの仕様とTypeScript独自の機能を両方押さえつつ、リテラル型・ユニオン型・keyof型・ジェネリクスなど、高度な型表現まで踏み込んで解説しています。TypeScriptの型システムの表現力を本格的に学べる一冊です。

オンラインで参照できる公式ドキュメント

TypeScript公式ハンドブック


https://www.typescriptlang.org/docs/
TypeScriptの公式ドキュメントです。
intersection型を含む、すべての型システムの機能について詳細な説明があります。

TypeScript Deep Dive


https://basarat.gitbook.io/typescript/
TypeScriptの深い部分まで掘り下げて解説しているオンラインブックです。
無料で読むことができ、intersection型についても詳しく説明されています。

TypeScriptの学習は終わりがありません。
新しい機能が常に追加され、より良い書き方が発見されています。
継続的に学習を続けることで、より良いTypeScriptプログラマーになれるはずです。


まとめ – Record型で有限キーを型安全に扱う

この記事の要点をまとめます。

  • Record型は、有限のキー集合と値の型をまとめて定義し、キーの網羅をコンパイラに強制できるユーティリティ型
  • 内部はMapped Typeで、各キーに同じ値の型を割り当てるため、欠けたキーを型エラーで検出できる
  • 使い分けは「有限キーで網羅したいならRecord」「動的・無限キーならインデックスシグネチャ」「形が固定で値の型を個別最適化するなら明示的なobject型」
  • Record<string, T> ではアクセス結果がundefinedになりうるため、noUncheckedIndexedAccess を有効にして型で守る
  • 有限キーのRecordはキー集合が固定され後から足せない点に注意し、任意化はPartial併用で対応する

キーが有限か無限か、網羅を強制したいかを起点に選べば、Recordとインデックスシグネチャ・object型の書き分けに迷わなくなります。


※本記事の本文案はAIを活用して作成していますが、記載している内容およびコードは筆者が実際に調査、検証・実行し、内容の正確性を確認した上で公開しています。