【実務・中級編】InterfaceのインデックスシグネチャとRecordの使い分け – TypeScript コア・型システムの基礎解析バイブル

コードレビューの現場から:`interface` のインデックスシグネチャ vs `Record`

開発プロジェクトでコードレビューをしていると、動的なキーを持つオブジェクトに遭遇した際、開発者が何の思想もなく `[key: string]: any` や `Record` を適当に使い分けているコードによく出会う。

「とりあえず動くから」で書かれたインデックスシグネチャは、TypeScriptの恩恵である型安全性を静かに、しかし確実にかみ砕いていく。

動的プロパティを持つオブジェクトを定義する際、我々には主に2つの選択肢が残されている。
1. `interface` が持つ インデックスシグネチャ(Index Signature)
2. ユーティリティ型 `Record`

この2つは一見すると「キーと値の型を指定する」という同じ目的を達成できるように見えるが、コンパイラの型評価プロセス、拡張性、そして安全性において根本的な違いが存在する。

今回は、フロントエンドのコンポーネント設計や非同期APIのペイロード処理において、バグの起きない堅牢な設計を選択するための極限の知見を伝授しよう。

—

1. 根本的な違い:コンパイラはどう評価しているか

まず、TypeScriptの型システムにおいて、これらがどのように表現され、評価されているかを解剖する。

Interfaceのインデックスシグネチャ

interface Dictionary {
[key: string]: string;
}

これは「このオブジェクトは、指定された型(ここでは `string`)のプロパティをいくつでも持ってよい」という構造的制約の宣言だ。ここで重要なのは、インデックスシグネチャを定義すると、「明示的に宣言された既知のプロパティ」もそのシグネチャの型に強制されるという強力な(そして時に厄介な)副作用が生じることである。

`Record`

type Dictionary = Record;

これはマップ型(Mapped Types)の糖衣構文(シンタックスシュガー)に他ならない。実体としては以下と同義である。

type Record = {
[P in K]: T;
};

有限のキーの集合 `K` から、値の型 `T` へのマッピングを動的に構築する。

—

2. なぜ `interface` のインデックスシグネチャでバグが起きるのか?

実務の現場でインデックスシグネチャが引き起こす最悪のバグは、「既知のプロパティの型制約がすり抜ける現象」だ。

以下のコードを見てほしい。

// 悪い例:Interfaceのインデックスシグネチャ
interface UserConfig {
theme: ‘light’ | ‘dark’; // リテラル型で厳格に縛りたい
[key: string]: string; // すべての動的キーは string でなければならない
}

const config: UserConfig = {
theme: ‘light’,
lang: ‘ja’,
};

// ここで何が起きるか?
// config.theme は本来 ‘light’ | ‘dark’ であるべきだが、
// インデックスシグネチャ [key: string]: string に適合させるため、
// TypeScriptの型チェッカーは `theme` の型を `string` に広げて(Widening)評価してしまう。

チーフアーキテクトとして断言するが、これはTypeScriptの型安全性の崩壊だ。厳密なユニオン型(`’light’ | ‘dark’`)を期待してコンポーネントに渡したつもりが、実際には単なる `string` として評価され、IDEの補完汚染やランタイムエラーの温床になる。

では、`Record` ではどうか?

TypeScriptの近代的なバージョン(4.x以降)では、マップ型および `Record` において、明示的なプロパティがインデックスシグネチャ的な挙動と衝突する場合、より厳格に扱われるか、あるいは型エラーとして検知される。特に、キーを限定した厳密なマッピングを行いたい場合、`Record` はその真価を発揮する。

—

3. 実務で即座に使える!堅牢な設計パターン

では、フロントエンドのAPI連携やコンポーネント設計において、どのように使い分けるべきか。プロダクションコードの基準となる設計パターンを提示する。

パターンA: 厳密なキーの網羅性が求められる場合(UIの状態管理・辞書データ)

キーの種類が有限に決まっている(例:多言語対応のキー、ステータスごとのカラーマッピング)場合は、`Record` を使うべきだ。さらに、`keyof` と組み合わせることで、「キーの網羅漏れをコンパイル時に検知する」ことが可能になる。

// — 堅牢な設計例: Recordによる網羅的マッピング —

type StatusCode = ‘200’ | ‘400’ | ‘500’;

// すべてのステータスに対するハンドリングを強制する
const statusMessages: Record = {
‘200’: ‘成功しました’,
‘400’: ‘リクエストが不正です’,
‘500’: ‘サーバーエラーが発生しました’,
// 仮に ‘404’ を追加し忘れたりすると、TypeScriptコンパイラが即座にエラーを吐く
};

export const getStatusMessage = (code: StatusCode): string => {
return statusMessages[code];
};

パターンB: 完全に未知の動的ペイロード(非同期APIレスポンス等)

バックエンドからスキーマの定まっていない任意のJSONオブジェクトが降ってくる場合や、プラグインの拡張設定など、キー名が完全に予測不可能な場合は、`Record` を選択する。

> ⚠️ 警告: `any` を使うのは素人の仕事だ。`unknown` を使い、アクセス時に型ガード(Type Guard)を強制せよ。

// — 堅牢な設計例: 未知のAPIレスポンスの安全な処理 —

type ApiPayload = Record;

function processPayload(payload: ApiPayload) {
// 安全な型narrowing(型ガード)
if (typeof payload[‘message’] === ‘string’) {
console.log(payload[‘message’].toUpperCase()); // 安全にstringとして扱える
}
}

パターンC: オブジェクト指向的な拡張性(OOPライクなクラスやミックスイン)

もしあなたがライブラリのコア部分を設計しており、ユーザーが独自のプロパティを自由に追加できる拡張ポイント(Extensibility)を提供したい場合、唯一 `interface` のインデックスシグネチャに存在価値が生まれる。

// — 拡張性を許容するInterface設計 —
interface PluginOptions {
enabled: boolean;
timeout?: number;
// ユーザー定義のカスタム設定を許容する
[extensionKey: string]: unknown;
}

—

4. パフォーマンスとコンパイル速度の知見

最後に、大規模コードベースにおけるパフォーマンスの話をしよう。

TypeScriptコンパイラ(`tsc`)は、型評価の際にメモリとCPUを消費する。

  • 複雑なマップ型(`Record` を多用し、条件分岐やテンプレートリテラル型を組み合わせたもの)は、型推論の深さを増大させ、IDE(VSCodeなど)の反応速度(Typecheck latency)を悪化させる原因になる。
  • 一方で、シンプルな `interface` のインデックスシグネチャは、コンパイラの内部表現において比較的軽量に処理される傾向がある。

しかし、「IDEのパフォーマンスのために型安全性を犠牲にする」というトレードオフは、プロダクションコードにおいては絶対に許されない。 パフォーマンス問題は、型設計を複雑化させすぎている(過剰なジェネリクスの乱用など)ことが原因であって、`Record` そのものが悪なわけではない。

—

まとめ:チーフアーキテクトからの提言

今日のコードレビューから、以下の基準をチームの共通認識として徹底してほしい。

1. キーが静的に決まっている、または網羅性を担保したい場合
👉 迷わず `Record` を使え。 リテラル型の崩壊を防ぎ、バグをコンパイル時にねじ伏せろ。
2. キーが完全に動的で、かつ拡張性を担保するプラグイン機構などの場合
👉 `interface` のインデックスシグネチャを使え。 ただし、値の型には `any` ではなく必ず `unknown` を指定し、ランタイムの安全性を担保しろ。

「動的な型だから適当でいいや」という妥協が、プロダクションの信頼性を静かに蝕む。TypeScriptを真に掌握したエンジニアであれば、動的領域ですさえも型システムの網の目を潜らせてはならない。

タイトルとURLをコピーしました