TypeScript でオブジェクトを扱っていると、「キーはすべて文字列、値はすべて数値にしたい」「'success'・'error'・'loading' という決まったキーだけを持つオブジェクトを作りたい」といった場面が出てきます。こうした「キーの型」と「値の型」をまとめて指定できるのが Record 型です。Record は TypeScript に最初から用意されているユーティリティ型で、自分で複雑な型を書かなくても、目的のオブジェクトの形をすっきり表現できます。この記事では、Record の基本形から、可変キー・固定キーの使い分け、インデックスシグネチャとの違い、keyof との組み合わせ、そして実務でよく使うパターンまでを初心者向けに解説します。
目次
Record 型の基本形
Record は Record<Keys, Type> という形で書きます。Keys にはキーの型、Type には値の型を指定します。「ユーティリティ型」とは、TypeScript が標準で用意している、既存の型から別の型を作るための便利な型のことです。Record はその中でも「キーと値の型を指定してオブジェクトの型を組み立てる」役割を持ちます。
// キーは string、値は number のオブジェクト
const scores: Record<string, number> = {
math: 90,
english: 80,
science: 75,
};
scores.history = 60; // OK: 新しい文字列キーを追加できる
// scores.math = 'A'; // エラー: 値は number でなければならない
Record<string, number> は「任意の文字列をキーに持ち、その値はすべて number であるオブジェクト」という意味です。値に文字列を入れようとするとコンパイルエラーになります。このように Record を使うと、{ キーの型: 値の型 } という形を短く書けるのが特徴です。次の表で、代表的な書き方と意味を整理しておきましょう。
| 記法 | 意味 |
|---|---|
Record<string, number> | 任意の文字列キー・値がすべて number のオブジェクト |
Record<string, string> | 任意の文字列キー・値がすべて string のオブジェクト |
Record<'a' | 'b', number> | キーが 'a' と 'b' だけに固定されたオブジェクト |
Record<string, User> | 文字列キーから User 型の値を引くマップ |
Record<string, number> のような可変キー
キーに string(や number)を指定すると、「どんな文字列でもキーにできる」オブジェクトになります。あらかじめキーが分からず、実行中に増えていくようなデータに向いています。たとえば「商品IDから商品オブジェクトを引く」といったマップ(対応表)がその代表例です。
type User = { id: string; name: string };
// id(文字列)から User を引くマップ
const usersById: Record<string, User> = {
u001: { id: 'u001', name: '佐藤' },
u002: { id: 'u002', name: '鈴木' },
};
console.log(usersById.u001.name); // '佐藤'
ここで注意したいのは、キーを string にした場合、存在しないキーにアクセスしても型の上ではエラーにならない点です。usersById.u999 と書くと型としては User ですが、実際には undefined が返ります。実行時の undefined まで型で防ぎたい場合は、tsconfig.json の noUncheckedIndexedAccess を有効にすると、値の型が自動的に User | undefined となり、存在チェックが促されます。可変キーの Record を使うときは、この挙動を覚えておくと安全です。
決まったキーだけを持つオブジェクトを作る
Keys に文字列リテラルのユニオン型を渡すと、「そのキーだけを持つ」オブジェクトの型になります。リテラル型とは 'success' のように「特定の値そのもの」を表す型、ユニオン型とは | でつないで「このうちのどれか」を表す型です。この2つを組み合わせてキーに指定すると、キーの数も名前も固定されたオブジェクトを作れます。
// 'success' | 'error' | 'loading' の3つのキーが必須
const messages: Record<'success' | 'error' | 'loading', string> = {
success: '保存しました',
error: 'エラーが発生しました',
loading: '読み込み中です',
};
console.log(messages.success); // '保存しました'
// loading を書き忘れるとエラー: プロパティ 'loading' がありません
この書き方では、3つのキーがすべて必須になります。どれか1つでも書き忘れるとコンパイルエラーになるため、「ステータスごとにメッセージを必ず用意する」といった網羅性を型で保証できます。逆に messages.warning のように定義していないキーへアクセスしようとしてもエラーになるので、タイプミスも防げます。可変キーの Record<string, string> と違い、キーが決まっているぶん安全性が高いのが特徴です。
インデックスシグネチャとの違いと使い分け
Record<string, number> は、インデックスシグネチャという書き方でも表現できます。インデックスシグネチャとは、{ [key: string]: number } のように「キーの型: 値の型」を角かっこで書く記法です。次の2つは、ほぼ同じ意味の型になります。
// Record を使った書き方
type ScoresA = Record<string, number>;
// インデックスシグネチャを使った書き方(ほぼ同じ意味)
type ScoresB = { [key: string]: number };
const a: ScoresA = { math: 90 };
const b: ScoresB = { math: 90 };
大きな違いは、キーを特定の名前に限定できるかどうかです。インデックスシグネチャのキーには string・number・symbol(およびそれらのテンプレートリテラル型)しか指定できず、'success' | 'error' のようなリテラルのユニオンを直接キーにはできません。一方 Record は Record<'success' | 'error', string> のように、決まったキーの集合をそのまま渡せます。使い分けの目安としては、キーが自由に増える可変マップならどちらでも書けますが、Record のほうが短く読みやすいことが多いです。キーを決まった名前に固定したいときは Record を選ぶ、と覚えておくとよいでしょう。
keyof と組み合わせて既存の型からキーを作る
Record は keyof と組み合わせると、さらに便利になります。keyof は、あるオブジェクト型が持つプロパティ名をすべてユニオン型として取り出す演算子です。これを Record のキーに渡せば、「既存の型のプロパティ名をそのままキーにした別のオブジェクト」を作れます。
type User = { id: string; name: string; age: number };
// User のプロパティ名('id' | 'name' | 'age')をキーにする
type UserLabels = Record<keyof User, string>;
const labels: UserLabels = {
id: 'ID',
name: '名前',
age: '年齢',
};
// id・name・age のいずれかを書き忘れるとエラーになる
keyof User は 'id' | 'name' | 'age' というユニオン型になります。これを Record のキーに渡すことで、「User の各プロパティに対応するラベルを、すべて漏れなく用意する」型が作れます。もし将来 User にプロパティを追加すれば、UserLabels 側も自動的にそのキーを要求するようになるため、片方だけ更新して対応を忘れる、といったミスを型で防げます。Record と keyof の組み合わせは、こうした「型どうしの整合性を保ちたい」場面で力を発揮します。
実務での使いどころ
最後に、実務でよく登場する Record の使い方をまとめておきます。1つ目は、これまで見てきた「ステータスごとのメッセージ辞書」です。決まった状態それぞれに文字列を対応させたいときに、キーの網羅を型で保証できます。2つ目は「id からオブジェクトを引くマップ」で、配列を id でまとめ直すときによく使います。
type Status = 'todo' | 'doing' | 'done';
// ステータスごとの表示ラベル辞書
const statusLabel: Record<Status, string> = {
todo: '未着手',
doing: '作業中',
done: '完了',
};
type Product = { id: string; name: string };
const products: Product[] = [
{ id: 'p1', name: 'りんご' },
{ id: 'p2', name: 'みかん' },
];
// 配列を id をキーにしたマップへ変換する
const productMap: Record<string, Product> = {};
for (const p of products) {
productMap[p.id] = p;
}
console.log(statusLabel.doing); // '作業中'
console.log(productMap.p1.name); // 'りんご'
辞書のようにキーが決まっているものはリテラルのユニオンをキーにした Record、実行時に増えるマップは Record<string, T>、という使い分けが基本の形です。可変キーのマップでは、前述のとおり存在しないキーへのアクセスに注意し、必要に応じて存在チェックや noUncheckedIndexedAccess を活用しましょう。
まとめ
Record<Keys, Type> は、キーの型と値の型をまとめて指定してオブジェクトの型を作るユーティリティ型です。Record<string, number> のようにキーへ string を渡せば「任意の文字列キー・共通の値の型」を持つ可変マップになり、Record<'success' | 'error' | 'loading', string> のようにリテラルのユニオンを渡せば「決まったキーだけを必須で持つ」オブジェクトになります。同じような表現ができるインデックスシグネチャ({ [key: string]: number })は、キーをリテラルの集合に限定できない点が異なり、決まったキーを扱いたいときは Record が適しています。さらに keyof と組み合わせれば、既存の型のプロパティ名からキーの集合を自動的に作れます。「キーの型」と「値の型」を意識して Record を使い分けると、辞書やマップの型がぐっと書きやすく、安全になります。