index-signature-alternatives について
TypeScript の型をより正確に設計するためのスキルです。既知のキーには interface や Record、実際に動的なキーには Map を使う方法を示します。
主な機能
- 01既知のプロパティに interface
- 02固定キー集合に Record
- 03動的なキーに Map
- 04省略可能な項目と有効な組み合わせのモデル化
- 05インデックスシグネチャの適切な用途の把握
活用シーン
- 01API や設定の型設計
- 02CSV などの動的データの表現
- 03任意のキーを許す型の見直し
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ marius-townhouse/ effective-typescript-skills --skill index-signature-alternativesインストール後、Claude Code で/index-signature-alternativesを入力して読み込まれたか確認します。
詳細
インデックスシグネチャよりも、より正確な代替手段を優先する
概要
インデックスシグネチャは不正確です。代わりにインターフェース、Record、またはMapを使用してください。
インデックスシグネチャ({[key: string]: T})では、任意の文字列キーを使用でき、特定のキーを必須にできず、キーごとに異なる型を指定できません。ほとんどの場合、より適切な代替手段があります。
このスキルを使用する場面
- プロパティ名が決まっている型を定義するとき
- APIや設定ファイルのデータをモデル化するとき
- CSVや動的データを扱うとき
- オブジェクト型とMapのどちらを使うか選ぶとき
絶対に守るべきルール
If you know the property names, DON'T use an index signature.
Use an interface, Record, or mapped type instead.
次の点を覚えておいてください:
- インデックスシグネチャでは任意のキー(タイプミスも含む)を使用できます
- インデックスシグネチャでは、特定のキーを必須にできません
- インデックスシグネチャでは、キーごとに異なる型を指定できません
- 言語サービス(自動補完)は、インデックスシグネチャではうまく機能しません
問題の検出:インデックスシグネチャの問題点
// Index signature: too permissive
type Rocket = { [property: string]: string };
const rocket: Rocket = {
name: 'Falcon 9',
variant: 'Block 5',
thrust: '7,607 kN',
};
// Problems:
rocket.Name; // Typo compiles (should be 'name')
const r: Rocket = {}; // Empty object is valid
rocket.thrust; // Can't be a number, even though it should be
より適切な代替手段
1. インターフェース(プロパティ名が決まっている場合に最適)
interface Rocket {
name: string;
variant: string;
thrust_kN: number; // Can have different types
}
const falconHeavy: Rocket = {
name: 'Falcon Heavy',
variant: 'v1',
thrust_kN: 15200,
};
// Benefits:
// - Typos caught: rocket.Name is an error
// - Required fields enforced
// - Each field has its own type
// - Autocomplete works
2. Record(キーが既知のユニオンの場合)
// Limited set of keys, same value type
type Vec3D = Record<'x' | 'y' | 'z', number>;
// Same as: { x: number; y: number; z: number }
type CSSColors = Record<'primary' | 'secondary' | 'accent', string>;
3. オプショナルプロパティ(一部の項目だけが存在する場合)
// When you know possible keys but not all will be present
interface Row {
a: number;
b?: number;
c?: number;
d?: number;
}
4. ユニオン型(組み合わせを正確に表す場合)
// When specific combinations are valid
type Row =
| { a: number }
| { a: number; b: number }
| { a: number; b: number; c: number };
5. Map(キーが本当に動的な場合)
// When keys are genuinely unknown at compile time
function parseCSV(input: string): Map<string, string>[] {
const lines = input.split('\n');
const [headerLine, ...rows] = lines;
const headers = headerLine.split(',');
return rows.map(rowStr => {
const row = new Map<string, string>();
rowStr.split(',').forEach((cell, i) => {
row.set(headers[i], cell);
});
return row;
});
}
const rockets = parseCSV(csvData);
const thrust = rockets[0].get('thrust_kN');
// ^? const thrust: string | undefined (safer!)
インデックスシグネチャが適切な場合
追加プロパティを許可する
interface ButtonProps {
title: string;
onClick: () => void;
[otherProps: string]: unknown; // Allow any extra props
}
renderButton({
title: 'Click me',
onClick: () => {},
theme: 'dark', // OK now
'data-testid': 'submit-btn', // OK
});
テンプレートリテラルによる制約
// Only allow keys starting with 'data-'
interface DataProps {
[key: `data-${string}`]: string;
}
const props: DataProps = {
'data-testid': 'my-button',
'data-value': '42',
// 'theme': 'dark', // Error! Key must start with 'data-'
};
Mapとインデックスシグネチャ付きオブジェクトの比較
| 機能 | Map | インデックスシグネチャ |
|---|---|---|
| .get()の戻り値 | T | undefined | T(安全ではない) |
| プロトタイプの問題 | なし | あり |
| 反復順序 | 保証される | 保証されない |
| 任意のキー型 | 対応 | 文字列/数値/シンボルのみ |
| TypeScriptのサポート | 良好 | より優れた自動補完 |
// Map is safer for dynamic data
const scores = new Map<string, number>();
const score = scores.get('alice');
// ^? const score: number | undefined
// Index signature pretends value always exists
const scoreObj: { [name: string]: number } = {};
const score2 = scoreObj['alice'];
// ^? const score2: number (but it's actually undefined!)
動的データを型に変換する
// Parse dynamic data, validate, return typed object
function parseRocket(map: Map<string, string>): Rocket {
const name = map.get('name');
const variant = map.get('variant');
const thrust_kN = Number(map.get('thrust_kN'));
if (!name || !variant || isNaN(thrust_kN)) {
throw new Error(`Invalid rocket: ${JSON.stringify([...map])}`);
}
return { name, variant, thrust_kN };
}
// Now you have type safety
const rockets = parseCSV(csvData).map(parseRocket);
// ^? const rockets: Rocket[]
圧力に対処するための手順
1. 「すべてのキーは把握できません」
圧力:「キーはユーザー入力/APIから取得します」
**回答:**本当に動的なデータにはMapを使用し、その後、型付きインターフェースに対して検証します。
**対応:**入力にはMap<string, string>を使用し、その後、インターフェースにパースします。
2. 「インデックスシグネチャのほうが簡単です」
圧力:「{[k: string]: any}を使って先に進めばいい」
**回答:**型安全性と自動補完がすべて失われます。
**対応:**コードが多少増えても、実際の構造を定義します。
危険信号 — 作業を止めて再検討する
- プロパティ名が決まっているのにインデックスシグネチャを使用している
[key: string]: anyをどこかで使用している- プロパティ名を入力するときに自動補完が表示されない
- プロパティ名のタイプミスがTypeScriptで検出されない
よくある正当化(どれも不適切)
| 言い訳 | 実際のところ |
|---|---|
| 「キーは動的です」 | 実際には、コンパイル時に判明していることがよくあります |
| 「プロパティが多すぎて列挙できません」 | Recordやマップ型で対応できます |
| 「ただの設定です」 | 設定にはスキーマがあります。定義しましょう |
クイックリファレンス
// DON'T: Index signature for known keys
type Bad = { [key: string]: string };
// DO: Interface for known keys
interface Good { name: string; value: string; }
// DO: Record for union of keys
type Colors = Record<'red' | 'green' | 'blue', number>;
// DO: Map for truly dynamic keys
const data = new Map<string, unknown>();
// DO: Index signature only for extra properties
interface Props {
required: string;
[extra: string]: unknown;
}
要点
インデックスシグネチャは、通常必要のない柔軟性と引き換えに、正確さを犠牲にします。
プロパティ名がわかっている場合は、インターフェースを使用してください。キーの集合が決まっている場合は、Recordを使用してください。キーが本当に動的な場合は、Mapを使用してください。追加プロパティを明示的に許可したい場合に限り、インデックスシグネチャを使用してください。
参考資料
Dan Vanderkam著『Effective TypeScript』項目16:「インデックスシグネチャよりも、より正確な代替手段を優先する」に基づいています。