【入門編】Interfaceの「インデックスシグネチャ」と「Record型」のパフォーマンス比較 – TypeScript コア・型システムの基礎解析バイブル

【TypeScript】Interfaceのインデックスシグネチャ vs Record型!コンパイル性能とIDE挙動で選ぶ決定版ガイド

こんにちは!TypeScriptの世界へようこそ。
普段からフロントエンドやNode.js、さらにはTypeScriptのコンパイラ内部の挙動まで追求している先輩エンジニアです。

TypeScriptで開発をしていると、「キー名が事前には決まっておらず、動的に変わるオブジェクト」を扱いたい場面によく遭遇しますよね。たとえば、APIから取得したユーザーIDをキーにした辞書データや、設定ファイルのマップ構造などです。

このとき、型定義の選択肢として主に次の2つが浮かびます。

1. Interface のインデックスシグネチャ (`interface Dict { [key: string]: number }`)
2. Utility Type の `Record` 型 (`type Dict = Record`)

「見た目が違うだけで、どっちを使っても同じじゃないの?」と思われがちですが、実はTypeScriptコンパイラ(`tsc`)の内部評価メカニズムや、IDE(VS Codeなど)の表示・補完速度、そして大規模開発でのコンパイルパフォーマンスに明確な差が生まれます。

今回は、プログラミング初学者や他言語から移行してきた皆さんが「なるほど!」と本質を理解できるように、基礎的な使い方からコンパイラ内部の評価コストの違いまで、分かりやすく紐解いていきますね。

ここをクリアすれば、TypeScriptの基本はバッチリマスターできますよ!

—

1. 基本をおさらい:2つの書き方と見た目の違い

まずは、それぞれの基本的な文法と意味を確認しましょう。

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

`interface`構文の中で `[key: T]: U` という特殊な記法を使います。

// Interfaceを使った動的オブジェクトの型定義
interface UserAgeMap {
// 「任意のstring型のキー」に対して「number型の値」が紐づくことを宣言
[userId: string]: number;
}

const userAges: UserAgeMap = {
“user-101”: 28,
“user-102”: 34,
};

// 呼び出しもスムーズです
const age = userAges[“user-101”]; // age の型は number

② Record型(型エイリアス)

TypeScriptが標準で提供しているジェネリック型 `Record` を使います。

// Record型を使った動的オブジェクトの型定義
type UserAgeMapRecord = Record;

const userAgesRecord: UserAgeMapRecord = {
“user-101”: 28,
“user-102”: 34,
};

const ageRecord = userAgesRecord[“user-101”]; // ageRecord の型は number

実行時のJavaScriptコードに変換されてしまえばどちらも単なるオブジェクトですが、コンパイル時(型チェック時)の評価プロセスが大きく異なります。

—

2. TypeScriptコンパイラ内部で何が起きているのか?

ここからが本題です。TypeScriptの型チェッカー(`checker.ts`)が、これらの型をどのように処理しているかをイメージ図で見てみましょう。

【Interface + インデックスシグネチャ】
[Interfaceの定義] ──> コンパイラが「名前付き型」としてメモリに保持
└─> 型の比較時:キャッシュされた参照を再利用(軽量・高速!)

【Record 型】
[Recordの呼び出し] ──> Genericの展開処理が発火!
└─> Mapped Type { [P in K]: T } を評価
└─> 評価結果の型をその都度生成(ほんの少しだけコスト高)

なぜInterfaceの方が型チェックが高速なのか?

1. 型キャッシュ(Memoization)の仕組み
TypeScriptコンパイラは、`interface` で定義された型を「名前付きの独立した型オブジェクト」として登録し、極めて強くキャッシュします。型チェック時に同じInterfaceが何度も参照される場合、コンパイラはキャッシュされたポインタを比較するだけで済むため、評価コストがほぼゼロになります。

2. Generic展開のオーバーヘッド
`Record` は、内部的には `type Record = { [P in K]: T }` というMapped Type(マップ型)として定義されています。
`Record` と書くたびに、コンパイラは「Generic型パラメタのエイリアスを展開し、Mapped Typeを評価する」というステップを挟みます。

数個〜数十個程度の型定義なら人間が体感できる差はありませんが、何千・何万行という巨大なプロジェクトになると、コンパイル時間やIDEのレスポンス速度に差が出てくるのです。

—

3. コンパイルパフォーマンス・ベンチマーク

実際にどれくらいの差が出るのか、概念的なベンチマーク実験を行ってみましょう。

大量の動的オブジェクト型を生成し、`tsc –extendedDiagnostics`(コンパイラの詳細診断コマンド)を実行した際の型評価時間を比較します。

// ─── 実験A: Interfaceを10,000個生成 ───
interface Dict0001 { [key: string]: number }
interface Dict0002 { [key: string]: number }
// … (10,000個続く)

// ─── 実験B: Record型を10,000個生成 ───
type Dict0001 = Record;
type Dict0002 = Record;
// … (10,000個続く)

測定結果のイメージ

| 評価指標 | Interface (インデックスシグネチャ) | Record型 (`Record`) |
| :— | :— | :— |
| Check Time(型チェック時間) | 早い(例: ~1.2秒) | やや遅い(例: ~1.8秒) |
| Instantiations(型インスタンス化数)| 少ない | 多い(Generic展開の分増加) |
| メモリ使用量 | 節約される | わずかに増加 |

コンパイラコアの視点から言うと、純粋なキー・バリューの辞書型であれば、`interface` を使った方が型チェッカーに優しいという結果になります。

—

4. IDE(VS Code)での開発体験(DX)と表示の違い

パフォーマンスだけでなく、日々の開発での「見え方」も大切なポイントですよね。マウスをホバーしたときの表示や、エラーメッセージの分かりやすさを比較してみましょう。

ツールチップ(ホバー表示)の違い

型の上にカーソルを置いたとき、VS Codeがどう表示するかを見てみましょう。

interface UserDict {
[id: string]: { name: string; age: number };
}

type UserDictRecord = Record;

// ① UserDict にホバーした場合
// 表示: interface UserDict
// 意図が明確で型名がシンプルに保たれます。

// ② UserDictRecord にホバーした場合
// 表示: type UserDictRecord = Record
// 中身の型がそのままインラインで展開されて表示されます。

  • Interfaceのメリット: 型名(`UserDict`)がそのまま表示されるため、複雑な型でもツールチップが散らかりません。
  • Record型のメリット: 型の中身(`Record`)が直接見えるため、どのような構造かが一目で分かる場合があります。

—

5. 決定的な違い!Record型にしかできないこと

ここまで「Interface優位」の話をしてきましたが、実はRecord型でしか実現できない超強力な機能があります。

それは、「キーの範囲をリテラル型のユニオン(Union)で制限する」ことです!

Interfaceではエラーになるパターン

Interfaceのインデックスシグネチャのキーには、`string` または `number`(および `symbol`)しか指定できません。特定のリテラル文字列の集まりをキーにすることは文法エラーになります。

type Role = “admin” | “user” | “guest”;

// ❌ 文法エラー(コンパイルエラー)になります!
// An index signature parameter type must be ‘string’, ‘number’, ‘symbol’, or a template literal type.
interface RolePermissions {
[key: Role]: boolean;
}

Record型ならスマートに書ける!

`Record` を使うと、キーを特定の値だけに限定した限定的なマップを完璧に定義できます。

type Role = “admin” | “user” | “guest”;

// ⭕️ 完全な型安全!すべてのキーが揃っているかもチェックされます
type RolePermissions = Record;

const permissions: RolePermissions = {
admin: true,
user: true,
guest: false,
// もし “superAdmin” などを足したり、”guest” を忘れたりすると型エラーになります!
};

初学者の皆さんが最も陥りやすい罠がここです!
「キーが決まった文字列のどれか(Union型)なら `Record` 一択」、「キーが無限に増える無制限の文字列なら `Interface`」 と覚えておくと間違いありません。

—

6. 実践:どちらを使うべき?使い分けの黄金ルール

最後に、現場で迷わないための判断フローチャートをまとめました。

【動的なキーを持つオブジェクトを作りたい!】
│
├─ Q1. キーは “admin” | “user” のような特定の文字の組み合わせ(Union型)ですか?
│ ├─ YES ──> 【 Record 】 を使おう! (例: Record)
│ └─ NO ──> Q2へ
│
└─ Q2. 拡張性(宣言マージ)や大規模なコンパイル速度を優先したいですか?
├─ YES ──> 【 Interface + インデックスシグネチャ 】 を使おう!
└─ NO ──> チームのコーディング規約に合わせればOK!

まとめ一覧表

| 観点 | Interface インデックスシグネチャ | Record型 (`Record`) |
| :— | :— | :— |
| 構文 | `interface D { [k: string]: V }` | `type D = Record` |
| 型チェック速度 | 🚀 非常に高速(高速にキャッシュ) | ⚡️ 普通(Generic展開コストあり) |
| Union型のキー | ❌ 不可 (`string`等の基本型のみ) | ⭕️ 可能 (`Record<"A" \| "B", V>`) |
| 宣言マージ(後勝ち拡張)| ⭕️ 可能 | ❌ 不可 |
| 主な用途 | 無制限な辞書データ、ライブラリの型 | 範囲が限定されたマップ、短文での定義 |

—

おわりに

TypeScriptの型システムは、単にコードのバグを防ぐだけでなく、コンパイラがどう型を計算し、どれだけ快適にエンジニアが補完機能を使えるかという深みを持っています。

  • 基本的な辞書オブジェクトなら Interface
  • 制限されたキーのマップなら Record型

この使い分けができるようになれば、あなたもTypeScriptの型システムを一段深いレベルで理解したプロエンジニアの仲間入りです!

焦らず一つずつ引き出しを増やしていきましょうね。応援しています!

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