TypeScript 速習チュートリアル

TypeScript インデックスシグネチャ

1. TypeScript における Index Signatures の理解

TypeScript の Index Signatures(インデックスシグネチャ) は、タイプセーフティを維持しながら、動的なプロパティ名を持つオブジェクトの型を定義するための強力な手段です。

これらを使用することで、実行時まで正確なプロパティ名が判明しない場合でも、ブラケット記法(obj[key])を介してアクセスされる値の型を指定できます。

1.1 主要なコンセプト

  • 動的なプロパティアクセス: 任意のプロパティ名を持つオブジェクトのハンドリング。
  • タイプセーフティ: 動的プロパティ全体で一貫した値の型を保証。
  • 柔軟なデータ構造: ディクショナリ、マップ、その他の動的データをモデリング。
  • ランタイムの安全性: コンパイル時に型関連のエラーをキャッチ。

2. 基本的な Index Signatures

2.1 String Index Signatures

インデックスシグネチャを使用すると、プロパティ名は事前には分からないものの、値の形状(Shape)が分かっているオブジェクトの型を定義できます。
インデックスシグネチャは、obj[key] のようなインデックスを介してアクセスされるプロパティの型を定義します。

コード例

// このインターフェースは、文字列のキーと文字列の値を持つオブジェクトを表します
interface StringDictionary {
  [key: string]: string;
}

// 定義に準拠したオブジェクトの作成
const names: StringDictionary = {
  firstName: "Alice",
  lastName: "Smith",
  "100": "One Hundred"
};

// プロパティへのアクセス
console.log(names["firstName"]); // "Alice"
console.log(names["lastName"]);  // "Smith"
console.log(names["100"]);       // "One Hundred"

// 新しいプロパティを動的に追加
names["age"] = "30";

// これはエラーを引き起こします
// names["age"] = 30; // エラー: 型 'number' を型 'string' に割り当てることはできません

インデックスシグネチャの構文では、ブラケット [key: type] を使用して許容されるプロパティ名(キー)の型を記述し、その後にそれらのプロパティが持つ値の型を記述します。

2.2 Number Index Signatures

TypeScript は、文字列と数値の両方のインデックスシグネチャをサポートしています。

コード例

// 数値インデックスを持つオブジェクト
interface NumberDictionary {
  [index: number]: any;
}

const scores: NumberDictionary = {
  0: "Zero",
  1: 100,
  2: true
};

console.log(scores[0]); // "Zero"
console.log(scores[1]); // 100
console.log(scores[2]); // true

// 複雑なオブジェクトの追加
scores[3] = { passed: true };

       注意: JavaScript では、数値のキーであっても、すべてのオブジェクトキーは内部的に文字列として保存されます。しかし、TypeScript は配列とオブジェクトを扱う際のロジカルなエラーをキャッチしやすくするために、これらを区別して扱います。

3. 高度な Index Signature のパターン

3.1 プロパティ型の混合

インデックスシグネチャと、明示的なプロパティ宣言を組み合わせることができます。

コード例

interface UserInfo {
  name: string; // 特定の名前を持つ必須プロパティ
  age: number;  // 特定の名前を持つ必須プロパティ
  [key: string]: string | number; // その他のプロパティは string または number である必要がある
}

const user: UserInfo = {
  name: "Alice",   // 必須
  age: 30,         // 必須
  address: "123 Main St", // オプション
  zipCode: 12345   // オプション
};

// これはエラーを引き起こします
// const invalidUser: UserInfo = {
//  name: "Bob",
//  age: "thirty", // エラー: 型 'string' を型 'number' に割り当てることはできません
//  isAdmin: true  // エラー: 型 'boolean' を型 'string | number' に割り当てることはできません
// };

重要: 明示的なプロパティをインデックスシグネチャと組み合わせる場合、明示的なプロパティの型は、インデックスシグネチャの値の型に割り当て可能である必要があります。

3.2 ReadOnly Index Signatures

インデックスシグネチャを readonly にすることで、作成後の変更を防ぐことができます。

コード例

interface ReadOnlyStringArray {
  readonly [index: number]: string;
}

const names: ReadOnlyStringArray = ["Alice", "Bob", "Charlie"];

console.log(names[0]); // "Alice"

// これはエラーを引き起こします
// names[0] = "Andrew"; // エラー: 'ReadOnlyStringArray' のインデックスシグネチャは読み取りのみを許可します

キーセットの制約や形状の変換については、Mapped Types のセクションを参照してください。

4. 実践的な活用例

4.1 API レスポンスのハンドリング

動的なキーを持つ API レスポンスの型定義に非常に有効です。

コード例

// 動的なキーを持つ API レスポンス用の型
interface ApiResponse<T> {
  data: {
    [resourceType: string]: T[];  // 例: { "users": User[], "posts": Post[] }
  };
  meta: {
    page: number;
    total: number;
    [key: string]: any;  // 追加のメタデータを許可
  };
}

// ユーザー API での使用例
interface User {
  id: number;
  name: string;
  email: string;
}

// モック API レスポンス
const apiResponse: ApiResponse<User> = {
  data: {
    users: [
      { id: 1, name: "Alice", email: "[email protected]" },
      { id: 2, name: "Bob", email: "[email protected]" }
    ]
  },
  meta: {
    page: 1,
    total: 2,
    timestamp: "2023-01-01T00:00:00Z"
  }
};

// データのアクセス
const users = apiResponse.data.users;
console.log(users[0].name);  // "Alice"

5. ベストプラクティス

5.1 推奨事項 (Do's and Don'ts)

  • Do: 動的なキーを持つコレクションにはインデックスシグネチャを使用する。
  • Do: 既知のフィールドについては、明示的なプロパティと組み合わせる。
  • Do: 値の型は可能な限り具体的に保つ(any を避ける)。
  • Do: 変更(Mutation)が不要な場合は readonly を使用する。
  • Don't: キーがあらかじめ分かっている場合は、固定のインターフェースを優先する。
  • Don't: すべてのプロパティがインデックスシグネチャの型に適合しなければならないことを忘れない。

5.2 よくある落とし穴

プロパティ名の競合

interface ConflictingTypes {
  [key: string]: number;
  name: string; // エラー: 文字列インデックス型 'number' に割り当てることはできません
}

interface FixedTypes {
  [key: string]: number | string;
  name: string;  // OK
  age: number;   // OK
}

Index Signatures vs. Record<K, T>

柔軟で動的なキーが必要な場合や、他のプロパティと混合する場合はインデックスシグネチャを使用します。簡潔でシンプルなマッピングには Record<K, T> を使用します。

// インデックスシグネチャ
interface StringMap {
  [key: string]: string;
}

// Record
type StringRecord = Record<string, string>;