TypeScriptの型安全性を崩壊させる「インデックスシグネチャ」の罠と、Record型による防衛術
フロントエンドの現場で、APIレスポンスの型定義に悩んだことはないか?
「とりあえず `[key: string]: any` や `[key: string]: string` で型を通しておけばいいか」――そう考えた瞬間、君たちのプロダクトの堅牢性は砂上の楼閣と化す。
今日は、TypeScriptの型システムの中でも特に「魔境」となりやすいインデックスシグネチャと、それを代替する`Record
—
1. インデックスシグネチャの「甘美な嘘」
まずは、多くのエンジニアがやりがちなこの定義を見てほしい。
interface UserConfig {
[key: string]: string; // ここが諸悪の根源
theme: ‘light’ | ‘dark’; // コンパイルエラー:’light’ | ‘dark’ は string に割り当て可能か?
}
このコードはコンパイルエラーになる。「インデックスシグネチャは、他のプロパティの型を包含しなければならない」という制約だ。これに従って無理やり `string` に合わせると、今度は「存在しないキーにアクセスしても `undefined` が返らない」という、TypeScriptの型安全性を根底から覆す挙動が発生する。
const config: UserConfig = { theme: ‘light’ };
// TypeScriptは「stringが返る」と嘘をつく
const val = config.unknownKey;
// 実行時:undefined が返るが、コンパイラは string 型と判断している
console.log(val.toUpperCase()); // 実行時エラー: Cannot read property ‘toUpperCase’ of undefined
結論:インデックスシグネチャは「キーが不明で、かつ値が常に存在することが保証されている」という極めて特殊な状況以外では、決して使うべきではない。
—
2. `Record` がもたらす真の型安全性
対して、`Record
`Record` は内部的に `Mapped Types` を利用しており、キーの存在可能性を明確に制御できる。
堅牢な設計:`Partial` の活用
APIからのレスポンスや、動的な設定オブジェクトを扱う場合、キーの存在確認を必須にするのがプロの流儀だ。
// 厳格な設定値の管理
type Theme = ‘light’ | ‘dark’;
// Record型でキーを明示的に制限する
type UserSettings = Record
const settings: Partial
header: ‘light’,
};
// 安全なアクセス
const headerTheme = settings.header; // Theme | undefined
if (headerTheme) {
console.log(headerTheme.toUpperCase()); // 安全!
}
`Record` を使う最大の利点は、「キーが未定義である可能性」をコンパイラに正しく認識させることにある。これにより、開発者は必ず `undefined` チェックを強制される。これが堅牢なコードへの第一歩だ。
—
3. 実務で差がつく:ユニオン型と組み合わせた最強のパターン
インデックスシグネチャを使いたくなるシーンの多くは、実は「キーが予め決まっている」ことが多い。その場合は、`Record` ではなく `Partial
// プロダクションで使える堅牢な設計パターン
type FieldName = ‘username’ | ‘email’ | ‘age’;
// フォームのバリデーション状態などを管理する
type FormErrors = Partial
const errors: FormErrors = {
username: [‘必須項目です’, ‘3文字以上で入力してください’],
};
// 存在しないキーへのアクセスも型安全
const emailError = errors.email; // string[] | undefined
この設計なら、もし `FieldName` に新しい項目が追加されても、コンパイラが「処理が漏れている箇所」を即座に指摘してくれる。インデックスシグネチャではこうはいかない。
—
チーフアーキテクトからの提言
コードレビューにおいて、私は以下の基準でインデックスシグネチャを検閲している。
1. `[key: string]: any` は禁止: それはTypeScriptを型のないJavaScriptに退化させる行為だ。
2. キーを特定できないか?: `Record
3. `undefined` を許容するか?: `Partial` を組み合わせて、呼び出し側に「値がないケース」のハンドリングを強制しているか。
TypeScriptは、君たちのコードの「曖昧さ」を徹底的に排除するためのコンパイラだ。その強力な武器を、インデックスシグネチャという名の怠慢で捨てるな。
今日のコードから、`[key: string]` を `Record` に置き換えてみてほしい。それだけで、君たちのアプリケーションの実行時エラーは劇的に減るはずだ。
型を制する者は、プロダクトの寿命を制する。 健闘を祈る。