TypeScript Record型とは何か?
TypeScriptはオブジェクト型を定義する方法はいくつかありますが、その中で「ユーティリティ型(Utility Types)」として提供されているRecord<K, T>は、頻繁で強力に使用されるものの一つです。
JavaScriptの本質はオブジェクトであり、キーと値のペアでデータを管理します。
しかし、動的なオブジェクトの扱いは、型安全性を損なう最大の要因でもあります。
Record型は、「どのキーが(K)」「どのような値を持つか(T)」を宣言的に定義することで、開発者が意図しないキーへのアクセスや、誤った型の値の代入をコンパイルレベルで完全に防ぐための武器となります。
本記事では、初学者がつまずきやすいポイントから、シニアエンジニアが設計時に考慮すべき「型安全性の限界」まで解説します。
Recordの基本構造と文法
TypeScriptを使いこなす上でRecord<K, T>は非常に重要な「ユーティリティ型」です。
ユーティリティ型とは、既存の型を変換して新しい型を作り出すための「便利なツール箱」のようなもので、TypeScriptが標準で提供しています。
Recordの役割を一言で表すと、「特定の型のキーを持つ、特定の型の値の集合(オブジェクト)を定義する」ことです。
上記を理解するために、引数であるKとTの確認します。
ジェネリクスの引数KとTの意味
Record<K, T>は、ジェネリクス(型の引数)を2つ取ります。
KとTに何を渡すかによって、生成されるオブジェクトの厳格さが決まります。
引数 K:プロパティの「キー(Keys)」を定義する
Kは、オブジェクトのプロパティ名として使用できる型を指定します。
TypeScriptの内部的な定義では、Kはkeyof anyを継承している必要があります。
つまり、以下の3つの型(またはそれらの組み合わせ)が許可されます。
- string: 文字列をキーにする(例:
Record<string, number>) - number: 数値をキーにする(例:
Record<number, string>) - symbol: シンボルをキーにする
引数 T:プロパティの「値(Type)」を定義する
Tは、各キーに対応するデータの型を指定します。
ここには制限がありません。
- プリミティブ型:
string,number,booleanなど - オブジェクト型: 自作の
interfaceやtype - 関数型:
() => voidなど - Union型:
string | numberなど
つまり、Record<string, User>と定義すれば、「キーは文字列で、その中身はすべてUserオブジェクトである」という辞書のような構造を保証できます。
基本的なコード例
実務で頻出する3つのパターンで具体的なコードを見ていきましょう。
- シンプルな文字列マッピング
- IDをキーとしたデータの管理(正規化)
- Union型による「キーの固定化」
シンプルな文字列マッピング
最も基本的な使い方は、ある名前に対して特定の値を割り当てる「連想配列」のような形式です。
// キー:色名(string)、値:16進数コード(string)
const colorCodes: Record<string, string> = {
red: "#FF0000",
blue: "#0000FF",
green: "#00FF00",
};
// 型エラーの例
// colorCodes.yellow = 255; // Error: number型はstring型に割り当てられませんIDをキーとしたデータの管理(正規化)
APIから取得したユーザーリストなどを、IDで素早く検索できるようにオブジェクト形式で保持する場合に非常に有効です。
type UserProfile = {
id: number;
name: string;
email: string;
};
// キー:ユーザーID(number)、値:UserProfileオブジェクト
const users: Record<number, UserProfile> = {
101: { id: 101, name: "鈴木", email: "suzuki@example.com" },
102: { id: 102, name: "田中", email: "tanaka@example.com" },
};
// users[103] にアクセスした際も、TypeScriptは「それはUserProfileである」と認識してくれますUnion型による「キーの固定化」
特定のステータスごとにメッセージを出し分けるような処理では、Record型とUnion型を組み合わせることで、「定義漏れ」を物理的に防ぐことができます。
type AppStatus = 'loading' | 'success' | 'error';
// AppStatusに含まれるすべてのキーを網羅しなければならない
const statusMessages: Record<AppStatus, string> = {
loading: "読み込み中です...",
success: "完了しました!",
error: "エラーが発生しました。",
};
// 仮に 'error' の定義を忘れると、TypeScriptが
// 「Property 'error' is missing in type...」と警告を出してくれます。Record型 vs インデックスシグネチャ:どちらを使うべき?
TypeScriptでオブジェクトの型を定義しようとした際、多くの開発者が最初にぶつかる壁が「Record<string, T>と{ [key: string]: T }(インデックスシグネチャ)のどちらを使うべきか?」という問題です。
結論から述べると、現代のTypeScript開発においては「Record型」の使用が推奨されるケースが圧倒的に多いです。
その境界線を明確に理解することは、保守性の高いコードを書くための必須知識です。
構文の比較
両者の見た目と内部構造の違いを整理しましょう。
| 特徴 | Record型 (Record<K, T>) | インデックスシグネチャ ({ [key: K]: T }) |
|---|---|---|
| 記述形式 | ユーティリティ型(関数のような記述) | オブジェクトリテラル内での直接記述 |
| 定義の柔軟性 | Union型やEnumをキーに指定可能 | 基本的に string か number のみ |
| 可読性 | 簡潔で「何を意図しているか」が明確 | JSのオブジェクトに近いが、記述が冗長 |
| 内部実装 | Mapped Types ({ [P in K]: T }) | 言語仕様としてのインデックスアクセス |
// Record型
type ScoreRecord = Record<string, number>;
// インデックスシグネチャ
type ScoreIndex = {
[key: string]: number;
};一見すると好みの問題に見えますが、次に説明する「キーの制限」において決定的な差が生まれます。
Record型を使うべき明確な理由
Record型が選ばれる理由は、「キーの型を具体的に絞り込める」という点にあります。
Union型との親和性が抜群(網羅性の保証)
インデックスシグネチャは、キーとして string や number という広い型しか受け付けません。
一方、Record型は特定の文字列の集合(Union型)をキーに指定できます。
type Weekday = 'Mon' | 'Tue' | 'Wed';
// Record型なら特定の曜日だけをキーに強制できる
const schedule: Record<Weekday, string> = {
Mon: "Meeting",
Tue: "Coding",
Wed: "Review"
};
// インデックスシグネチャでは以下のような記述はエラーになる
// type BadSchedule = { [key: Weekday]: string }; // Error!「特定のキー以外を許さない、かつ全てのキーを埋めることを強制する」という挙動は、設定ファイルやステート管理において最強のガードとなります。
記述の簡潔さと「DRY原則」の維持
Record型はユーティリティ型であるため、他の型と組み合わせてネストさせる際も非常にスマートに書けます。
Record<string, Record<string, number>> のように書けば、二次元配列のような構造も一目で理解できます。
これをインデックスシグネチャで書くと、中括弧が連続し、視認性が著しく低下します。
Mapped Typesの恩恵を受けられる
Record型は内部的に Mapped Types を利用しています。
これにより、既存の型(interfaceなど)から keyof を使って動的にキーを生成し、別の型をマッピングするといった高度な操作が容易になります。
インデックスシグネチャが適しているケース
Record型が推奨されるとはいえ、インデックスシグネチャが「現役」である理由も存在します。
主に以下の2つのケースです。
- インターフェース(interface)内で他のプロパティと共存させたい場合
- 再帰的なデータ構造の定義
- 既存のJavaScriptライブラリとの互換性
インターフェース(interface)内で他のプロパティと共存させたい場合
interface を使って特定のプロパティを定義しつつ、それ以外の動的なプロパティも許可したい場合はインデックスシグネチャが必要です。
interface UserMetadata {
id: number;
name: string;
[key: string]: any; // idとname以外に、動的な追加フィールドを許可
}Record型はオブジェクト全体を覆ってしまうため、このような「特定のキー + 自由なキー」という構造を定義するには向きません。
再帰的なデータ構造の定義
非常に複雑、あるいは階層が無限に続くような再帰的なオブジェクトを定義する場合、インデックスシグネチャの方が型の循環参照エラーを回避しやすいことがあります。
(※ただし、これも2026年現在のTSバージョンではRecord型で対応できる範囲が広がっています)。
既存のJavaScriptライブラリとの互換性
古くからあるJSライブラリの型定義ファイル(@types)では、インデックスシグネチャが多用されています。
それらの定義を拡張したり、模倣したりする際には、あえて記述を合わせることで混乱を避けるという戦略的な判断もあり得ます。
Record型の応用パターン
Record<K, T>は単体で使うことではなく、TypeScriptの他の強力な型機能(Union Types, Enums, Mapped Typesなど)と組み合わせた時に発揮されます。
より実務的で高度な「型安全」を実現するための応用パターンを習得しましょう。
エンジニアが現場で頻繁に利用する3つの設計パターンを解説します。
- Union Type(限定されたキー)との組み合わせ
- Enumやkeyofを用いた厳格な定義
- ネストしたRecord型の設計
Union Type(限定されたキー)との組み合わせ
Record型において最も強力かつ最も多用されるのが「Union Type(連合型)」をキーに指定するパターンです。
これにより、オブジェクトのプロパティを特定の文字列だけに制限し、なおかつ「定義漏れ」を許さない厳格な契約を結ぶことができます。
網羅性チェック(Exhaustiveness Checking)の威力
例えば、アプリケーションのカラーテーマを管理するオブジェクトを作成する場合を考えてみましょう。
type ThemeMode = 'light' | 'dark' | 'sepia';
// ThemeModeに含まれるすべてのキーを定義しなければエラーになる
const themeColors: Record<ThemeMode, string> = {
light: '#FFFFFF',
dark: '#000000',
sepia: '#F4ECD8',
};将来的に ThemeMode に 'high-contrast' という新しい値が追加された際、themeColors の定義箇所ですぐにコンパイルエラーが発生することです。
「デザイン変更時に特定のモードだけ色が設定されていなかった」というランタイムのバグを開発中に100%未然に防ぐことが可能になります。
Enumやkeyofを用いた厳格な定義
文字列を直接定義する以外にも、既存の定義を「ソース・オブ・トゥルース(信頼できる唯一の情報源)」としてRecord型を構成する手法があります。
Enum(列挙型)をキーにする
バックエンドのAPI定義などで Enum が使われている場合、それと同期したフロントエンドの表示用マッピングを簡単に作成できます。
enum UserStatus {
Active = 'ACTIVE',
Inactive = 'INACTIVE',
Pending = 'PENDING',
}
const statusLabels: Record<UserStatus, string> = {
[UserStatus.Active]: '有効',
[UserStatus.Inactive]: '無効',
[UserStatus.Pending]: '承認待ち',
};keyofを用いた動的なRecord生成
すでにある interface や type のキーをそのままRecord型のキーとして流用することも可能です。
interface User {
id: string;
name: string;
email: string;
}
// Userのプロパティ名をキーにし、その説明文を値に持つオブジェクト
const userFieldDescriptions: Record<keyof User, string> = {
id: "ユーザーを一意に識別するID",
name: "ユーザーのフルネーム",
email: "連絡先メールアドレス",
};keyof User を使うことで、User インターフェースに新しいフィールドが追加された際に、説明文の定義(userFieldDescriptions)も更新することをTypeScriptが強制してくれます。
ネストしたRecord型の設計
複雑なデータ構造を扱う際、Record型の中にさらにRecord型を配置する「ネスト(階層化)」が必要になることがあります。
これは、マトリックス状のデータや、多次元の設定ファイルを定義する際に非常に有効です。
多次元マッピングの例
例えば、「国」と「都市」の二段階で情報を管理するオブジェクトを定義してみます。
type Country = 'Japan' | 'USA';
type Category = 'Capital' | 'Population';
const statistics: Record<Country, Record<Category, string | number>> = {
Japan: {
Capital: 'Tokyo',
Population: 125000000,
},
USA: {
Capital: 'Washington D.C.',
Population: 331000000,
},
};Partialとの組み合わせで柔軟性を持たせる
全てのキーが必ずしも埋まるとは限らない場合は、Partial ユーティリティ型と組み合わせるのが実務的な解法です。
// すべての国にすべてのカテゴリデータがあるとは限らない場合
const incompleteStats: Record<Country, Partial<Record<Category, string | number>>> = {
Japan: {
Capital: 'Tokyo',
// Population は定義しなくてもエラーにならない
},
USA: {},
};実践的なユースケース:現場でどう使うか?
重要なのは「実務のどのような場面で Record<K, T> を導入すべきか」という判断基準です。
TypeScriptを導入しているモダンな現場では単なるデータの入れ物としてだけでなく、「ロジックの簡素化」と「不具合の未然防止」を目的として Record型が多用されます。
- APIレスポンスやステートの正規化
- 設定オブジェクトやマッピング処理
- ReactのPropsやスタイル定義
APIレスポンスやステートの正規化
フロントエンド開発、特に大規模なアプリケーションにおいて、データの「正規化(Normalization)」はパフォーマンスと保守性の向上に直結します。
配列からIDベースの辞書オブジェクトへ
APIから取得したデータが以下のような配列形式であることは珍しくありません。
const users = [{ id: 'u1', name: 'Alice' }, { id: 'u2', name: 'Bob' }];
これを Record 型を用いてIDをキーにしたオブジェクトに変換することで、高速アクセスが可能になります。
interface User {
id: string;
name: string;
email: string;
}
// キー:ユーザーID、値:Userオブジェクト
type UserState = Record<string, User>;
const usersById: UserState = {
"u1": { id: "u1", name: "Alice", email: "alice@example.com" },
"u2": { id: "u2", name: "Bob", email: "bob@example.com" },
};
// 検索が圧倒的に速く、コードも簡潔になる
const targetUser = usersById["u1"];状態管理ライブラリ(Redux / Zustand)との相性
ReduxやZustandで「現在選択されているアイテム」を管理する際、IDだけを保持し、Record形式の「Entities」から取得するパターンは、クリーンな設計の定石です。
データの重複を避け、一箇所を更新すれば全画面に反映される「信頼できる唯一の情報源(Single Source of Truth)」を実現できます。
設定オブジェクトやマッピング処理
複雑な if-else や switch 文を排除し、コードの宣言的な可読性を高めるために Record は非常に有効です。
ステータスからラベルへの変換
例えば、注文ステータスに応じた日本語ラベルやバッジの色を決定する処理を考えてみましょう。
type OrderStatus = 'pending' | 'shipped' | 'delivered' | 'cancelled';
// 各ステータスに対する設定を一箇所に集約
const ORDER_STATUS_CONFIG: Record<OrderStatus, { label: string; color: string }> = {
pending: { label: '保留中', color: 'gray' },
shipped: { label: '発送済み', color: 'blue' },
delivered: { label: '配達完了', color: 'green' },
cancelled: { label: 'キャンセル', color: 'red' },
};
// 使用側
const currentStatus: OrderStatus = 'shipped';
const { label, color } = ORDER_STATUS_CONFIG[currentStatus];Record でマッピングを定義しておけば、ロジック部分で複雑な条件分岐を書く必要がなくなり、新しいステータスが増えた際も ORDER_STATUS_CONFIG に追記するだけで済みます。
追記を忘れた場合はTypeScriptがエラーで教えてくれるため、「条件の漏れ」が物理的に発生しません。
ReactのPropsやスタイル定義
Reactコンポーネントの設計においても、Record はコードをクリーンに保つための魔法のツールとなります。
動的なスタイル管理
インラインスタイルや、CSS-in-JS(Styled-componentsなど)を利用する際、特定の状態に関連付けられたスタイルを Record で管理します。
type ButtonVariant = 'primary' | 'secondary' | 'danger';
const VARIANT_STYLES: Record<ButtonVariant, React.CSSProperties> = {
primary: { backgroundColor: 'blue', color: 'white' },
secondary: { backgroundColor: 'white', color: 'blue', border: '1px solid blue' },
danger: { backgroundColor: 'red', color: 'white' },
};
const Button: React.FC<{ variant: ButtonVariant; children: React.ReactNode }> = ({ variant, children }) => {
return <button style={VARIANT_STYLES[variant]}>{children}</button>;
};多言語対応(i18n)の簡易実装
専用のライブラリを入れるまでもない小規模なプロジェクトでは、Record を使って言語ごとの文言を管理するのが最も手軽で安全です。
type Language = 'ja' | 'en';
type MessageKey = 'welcome' | 'logout' | 'submit';
const MESSAGES: Record<Language, Record<MessageKey, string>> = {
ja: { welcome: 'ようこそ', logout: 'ログアウト', submit: '送信' },
en: { welcome: 'Welcome', logout: 'Logout', submit: 'Submit' },
};二重の Record 構造により、「日本語にはあるのに英語にはこのメッセージが定義されていない」といったミスを完全に排除できます。
Record型の罠と注意点
Record<K, T> は非常に強力なツールですが、TypeScriptが本来提供してくれるはずの「安全性」を自ら捨ててしまうことになりかねません。
特にランタイム(実行時)の挙動と型定義の乖離は、大規模なアプリケーションにおいて深刻なバグの温床となります。
ここでは、現場のエンジニアが一度はハマる「Record型の罠」と、その回避策について解説します。
Recordの安全性の落とし穴
Record型の最も一般的な使い方は Record<string, T> ですが、ここには大きな「型安全性の嘘」が隠れています。
存在しないキーへのアクセス問題
TypeScriptのデフォルト設定では、Record<string, T> と定義されたオブジェクトに対してどのような文字列でアクセスしても、型は T であると見なされてしまいます。
const scores: Record<string, number> = {
math: 90,
english: 80
};
// 存在しないキー 'science' にアクセス
const scienceScore = scores.science;
// TypeScript上の型は 'number' だが、実際の実行時の値は 'undefined'!
console.log(scienceScore.toFixed(1)); // 実行時に TypeError: Cannot read property 'toFixed' of undefined「型定義上は存在することになっているが、実際には存在しない」という状態が発生します。
TypeScriptが静的解析において、すべての可能性のある文字列キーを事前に把握できないために起こる仕様上の限界です。
現在のモダンな開発環境では、tsconfig.json の設定で noUncheckedIndexedAccess: true を有効にすることが強く推奨されます。
Object.keysやObject.entriesとの相性問題
TypeScriptを使い始めた多くの人が驚くのが、Object.keys() を使った時の挙動です。
なぜ string[]になってしまうのか?
例えば、キーを特定のUnion型に絞った Record であっても、Object.keys() の戻り値はそのUnion型の配列にはなりません。
type Category = 'food' | 'drink';
const stock: Record<Category, number> = {
food: 10,
drink: 5
};
// keys の型は 'Category[]' ではなく 'string[]' になる
const keys = Object.keys(stock);「構造的部分型(Structural Subtyping)」というTypeScriptの根本的な設計思想が関わっています。
TypeScriptでは、型定義にない余計なプロパティを持っているオブジェクトであっても、必要なプロパティさえ満たしていればその型として扱えてしまいます。
そのため、「実行時には定義外のキーが含まれている可能性がある」と判断され、安全のために string[] と返されます。
ループ処理などでどうしても型を一致させたい場合は、以下のようにアサーション(型の上書き)を行うのが一般的です。
(Object.keys(stock) as Category[]).forEach(key => {
console.log(stock[key]); // これでエラーが出なくなる
});解決策:Partial との組み合わせ
「すべてのキーが必ずしも埋まっているわけではない」という現実のデータ構造に対して、Record<K, T> は厳格すぎることがあります。
その際に役立つのが Partial とのコンビネーションです。
「疎なデータ(Sparse Data)」を正しく表現する
例えば、100種類のアイテムがあるうち、ユーザーが所持しているものだけを記録するオブジェクトを考えます。
type ItemId = string; // 本来はもっと具体的なUnion型を想定
interface ItemDetail { name: string; power: number; }
// これだと「全てのItemIdに対して必ずデータがある」ことになってしまう(危険)
// type Inventory = Record<ItemId, ItemDetail>;
// 正解:Partialを使って「あってもなくても良い(undefinedの可能性がある)」ことを明示する
type Inventory = Partial<Record<ItemId, ItemDetail>>;
const myInventory: Inventory = {
"item_001": { name: "鋼の剣", power: 50 }
};
const item = myInventory["item_999"];
if (item) {
// ここではじめて安全に item.name にアクセスできる
console.log(item.name);
}高度なテクニック:Mapped Typesとの関係
TypeScriptの Record<K, T> を理解し自在に操るためには、「Mapped Types(マップ型)」という強力な言語機能を避けて通ることはできません。
現在のモダン開発で欠かせない「テンプレートリテラル型」との高度な連携テクニックまでを解説します。
Record型の内部実装を紐解く
Record<K, T> は、実はTypeScriptの組み込みライブラリ(lib.es5.d.tsなど)の中で、わずか一行で定義されています。
Mapped Typesと呼ばれる構文を用いたユーティリティ型です。
type Record<K extends keyof any, T> = {
[P in K]: T;
};テンプレートリテラル型との連携
モダン開発においての応用例が、「テンプレートリテラル型(Template Literal Types)」との連携です。
これにより、文字列のパターンに基づいた型定義を自動生成することが可能になります。
プレフィックス(接頭辞)付きキーの自動生成
例えば、APIから取得したデータをフロントエンドの特定のコンポーネント用へ変換する際、キーに data_ という接頭辞を付けたい場合があります。
type BaseKey = 'id' | 'name' | 'email';
// 'data_id' | 'data_name' | 'data_email' をキーに持つRecord型を自動生成
type DataRecord = Record<`data_${BaseKey}`, string | number>;
const user: DataRecord = {
data_id: "u123",
data_name: "suzuki",
data_email: "suzuki@example.com"
};イベントハンドラの型定義への応用
Vue.jsやReactのProps設計において、「イベント名に基づいたハンドラ関数」を網羅したい際にもこのテクニックが輝きます。
type Action = 'update' | 'delete' | 'create';
// 'onUpdate' | 'onDelete' | 'onCreate' という関数を強制する
type EventHandlers = Record<`on${Capitalize<Action>}`, (id: string) => void>;
const handlers: EventHandlers = {
onUpdate: (id) => console.log(`${id}を更新`),
onDelete: (id) => console.log(`${id}を削除`),
onCreate: (id) => console.log(`${id}を作成`),
};
