【入門編】インデックスシグネチャとRecord型の使い分け:動的なオブジェクトを型安全に扱う – TypeScript コア・型システムの基礎解析バイブル

キーが不定なオブジェクトを型安全に扱う:インデックスシグネチャ vs Record

皆さん、こんにちは!TypeScriptの世界へようこそ。私は普段、TypeScriptのコア開発に携わったり、大規模なフロントエンドやNode.jsアプリケーションのアーキテクチャ設計をしたりしています。今日は、皆さんがTypeScriptで開発を進める上で、きっと「おっ!」となるような、でもちょっと悩むかもしれないテーマについて、じっくりと、そして丁寧に解説していきたいと思います。

「キーがどんな文字列になるか分からない、でも値の型は決まっている」というような、動的なオブジェクトを扱う場面って、開発していると結構ありますよね?例えば、ユーザーの設定情報、APIからのレスポンス、あるいは動的に生成されるデータなどです。

そんな時、TypeScriptでどうやって型を定義すればいいんだろう?と迷った経験はありませんか?

実は、この「キーが不定なオブジェクト」を型安全に扱うための強力なツールが2つあります。それが「インデックスシグネチャ」と「Record」です。

「どっちを使えばいいの?」「違いは何?」と思っているあなた、ご安心ください。この記事を読み終える頃には、この2つの使い分けがバッチリできるようになり、TypeScriptの基本マスターへの道がさらに開けるはずですよ!

それでは、一緒にTypeScriptの奥深い世界へ探求の旅に出かけましょう!

—

1. 動的なオブジェクトって、どんなもの?

まずは、私たちがこれから扱う「キーが不定なオブジェクト」が具体的にどんなものなのか、イメージを掴んでみましょう。

例えば、こんなJavaScriptのオブジェクトを考えてみてください。

// ユーザーの設定情報(例)
const userSettings = {
theme: ‘dark’,
fontSize: 16,
notificationsEnabled: true,
language: ‘ja’
};

// APIからのレスポンス(例)
const apiResponse = {
‘user-id’: 12345,
‘last-login’: ‘2023-10-27T10:00:00Z’,
‘session-token’: ‘abcdef123456’
};

これらのオブジェクトは、

  • キー(プロパティ名)が、あらかじめ全て決まっているわけではない。 (APIレスポンスのキーは、サービスごとに異なる場合もありますよね)
  • キーは文字列(またはシンボル)である。
  • 値の型は、ある程度決まっている。 (例: `userSettings` では `string` や `number`、`boolean` ですが、ここでは一旦「文字列か数値」のようにまとめて考えます)

という特徴を持っています。

JavaScriptの時点では、これらのオブジェクトをそのまま扱えますが、TypeScriptで型をつけようとすると、少し工夫が必要になります。なぜなら、TypeScriptは原則として、オブジェクトのプロパティは事前に定義されているものとして型チェックを行うからです。

ここで、インデックスシグネチャとRecordの出番というわけです!

—

2. インデックスシグネチャ:オブジェクトの「型」を定義する

インデックスシグネチャは、オブジェクトのプロパティの型そのものを定義する方法です。まるで、「このオブジェクトは、どんな文字列キーでも、それに紐づく値は〇〇型ですよ」と宣言するようなイメージです。

2.1. 基本的な使い方

インデックスシグネチャは、型定義の中に `[キーの型]: 値の型` という形で記述します。

// 例1: ユーザー設定オブジェクトの型定義
interface UserSettings {
[key: string]: string | number | boolean; // どんな文字列キーでも、値は string, number, boolean のいずれか
// 他に固定のプロパティがあれば、ここに追加できます
// userId: number; // 例: 固定の userId プロパティ
}

// 例2: APIレスポンスの型定義
interface ApiResponse {
[key: string]: string | number; // どんな文字列キーでも、値は string か number
}

コードの意味を深掘りしましょう:

  • `[key: string]`: これは「キーは文字列型である」ということを示しています。`key` の部分は任意の識別子(通常は `key` や `index` など)を使いますが、意味するところは「キーの型」です。
  • `: string | number | boolean`: これは「そのキーに紐づく値の型は、`string`、`number`、または `boolean` のいずれかである」ということを示しています。

この型定義を使うと、以下のようにオブジェクトを定義できます。

// 型定義に沿ったオブジェクト
const settings: UserSettings = {
theme: ‘dark’,
fontSize: 16,
notificationsEnabled: true,
language: ‘ja’
};

// 型定義に沿わないオブジェクト(エラーになります!)
// const invalidSettings: UserSettings = {
// theme: ‘dark’,
// age: 30, // age の値は number ですが、UserSettingsで許可されている型 (string | number | boolean) に含まれています。
// // ここで、たとえば { [key: string]: string } と定義していたら、age: 30 はエラーになります。
// };

// 値の型が合わない場合(エラーになります!)
// const settingsWithError: UserSettings = {
// theme: ‘dark’,
// fontSize: 16,
// // invalidValue: null // null は string | number | boolean のどれにも当てはまらないためエラー
// };

ポイント:

  • インデックスシグネチャは、オブジェクトが持つ可能性のあるすべてのプロパティの型を包括的に定義します。
  • 固定のプロパティとインデックスシグネチャを併用することも可能です。その場合、固定プロパティの型はインデックスシグネチャで許容される型に含まれている必要があります。

2.2. 陥りやすい文法エラーと注意点

インデックスシグネチャを使う上で、いくつか注意しておきたい点があります。

1. 値の型が広すぎる、または狭すぎる

interface StrictSettings {
[key: string]: string; // 値はすべて文字列でなければならない
}

const mySettings: StrictSettings = {
name: ‘Alice’,
// count: 10 // 10 は number 型なのでエラー!
isActive: ‘true’ // ‘true’ は string 型なのでOK
};

もし、`count` のように数値も入れたい場合は、値の型を `string | number` のように広げる必要があります。

2. キーの型は `string`、`number`、`symbol` のみ

インデックスシグネチャのキーの型には、`string`、`number`、`symbol` しか指定できません。

// NG: キーの型が string 以外
// interface InvalidKeyType {
// [key: boolean]: string; // エラー!
// }

これは、JavaScriptのオブジェクトのキーが、最終的に文字列(またはシンボル)に変換されるためです。`number` をキーとして指定した場合も、内部的には `string` に変換されて扱われます。

const obj: { [key: number]: string } = {
1: ‘one’, // これは { ‘1’: ‘one’ } と同じように扱われます
2: ‘two’
};

console.log(obj[1]); // ‘one’
console.log(obj[‘1’]); // ‘one’

3. 固定プロパティとの兼ね合い

インデックスシグネチャと固定プロパティを併用する場合、固定プロパティの型は、インデックスシグネチャで許容される型に含まれている必要があります。

interface UserProfile {
userId: number;
[key: string]: string | number; // userId (number) は string | number に含まれるのでOK
}

// OK
const profile1: UserProfile = {
userId: 123,
username: ‘alice’,
age: 30
};

// NG: 固定プロパティの型がインデックスシグネチャの型と合わない
// interface InvalidProfile {
// admin: boolean; // boolean は string | number に含まれない!
// [key: string]: string | number;
// }

この場合、`admin: boolean` を許容したいなら、インデックスシグネチャの値の型を `string | number | boolean` に広げる必要があります。

—

3. Record:型レベルで「キー」と「値」の関係を定義する

次に、Record 型を見ていきましょう。こちらは、TypeScript 2.1 から導入された、ユーティリティ型と呼ばれるものです。

Record は、キーの型 (`K`) と値の型 (`V`) を指定して、それらの組み合わせで構成されるオブジェクト型を生成します。

3.1. 基本的な使い方

Record は、以下のように使います。

// 例1: ユーザー設定オブジェクトの型定義 (Record を使用)
// K: string (キーは文字列)
// V: string | number | boolean (値は string, number, boolean のいずれか)
type UserSettingsRecord = Record;

// 例2: APIレスポンスの型定義 (Record を使用)
// K: string (キーは文字列)
// V: string | number (値は string か number)
type ApiResponseRecord = Record;

これらの型定義は、先ほどのインデックスシグネチャを使った例と、ほとんど同じように使えます。

const settings: UserSettingsRecord = {
theme: ‘dark’,
fontSize: 16,
notificationsEnabled: true,
language: ‘ja’
};

const apiResponse: ApiResponseRecord = {
‘user-id’: 12345,
‘last-login’: ‘2023-10-27T10:00:00Z’
};

// 値の型が合わない場合(エラーになります!)
// const invalidSettings: UserSettingsRecord = {
// theme: ‘dark’,
// invalidValue: null // null は string | number | boolean のどれにも当てはまらないためエラー
// };

Record の強力な点:

Record の真価は、キーの型 `K` にユニオン型やリテラル型を指定できる点にあります。これにより、より限定されたキーの集合に対して型を定義できます。

// 特定のキーのみを許可する例
type UserRoles = ‘admin’ | ‘editor’ | ‘viewer’;

// UserRole のいずれかの文字列をキーとし、値は boolean とするオブジェクト型
type UserPermissions = Record;

const adminPermissions: UserPermissions = {
admin: true,
editor: false,
viewer: false
};

// NG: 許可されていないキー ‘guest’ を指定するとエラー
// const guestPermissions: UserPermissions = {
// admin: false,
// guest: true // Error: Type ‘”guest”‘ is not assignable to type ‘”admin” | “editor” | “viewer”‘.
// };

// NG: 値の型が boolean でない場合もエラー
// const invalidPermissions: UserPermissions = {
// admin: true,
// editor: ‘yes’ // Error: Type ‘string’ is not assignable to type ‘boolean’.
// };

このように、Record を使うと、「どんなキーがあって、それぞれのキーにどんな値が対応するべきか」 を、より具体的に、そして型レベルで厳密に定義できるのです。

3.2. 陥りやすい文法エラーと注意点

Record も、いくつか注意しておきたい点があります。

1. キーの型 `K` に含められるもの

`K` には、`string`、`number`、`symbol`、あるいはそれらのユニオン型やリテラル型を指定できます。

// string のユニオン型
type Status = ‘pending’ | ‘processing’ | ‘completed’;
type StatusMap = Record; // キーは ‘pending’, ‘processing’, ‘completed’ のいずれか

// number のリテラル型
type ErrorCodes = 400 | 404 | 500;
type ErrorMessages = Record; // キーは 400, 404, 500 のいずれか

2. Record とインデックスシグネチャの基本的な使い分け

  • インデックスシグネチャ `[key: string]: T`:
  • 「どんな文字列キーでも、値は `T` 型」 という、包括的で緩やかな制約を定義したい場合に適しています。
  • キーの候補が非常に多い、あるいは未知数である場合に便利です。
  • Record
  • 「キーが `K` の型で、値が `V` 型」 という、より具体的なキーと値のペアの関係を定義したい場合に強力です。
  • 特に、`K` にユニオン型やリテラル型を指定することで、許可されるキーを限定したい場合に非常に有効です。

3. Record はインデックスシグネチャ `[key: string]: T` とほぼ同等

`Record` は、実質的に `[key: string]: T` と同じような意味合いになります。どちらを使っても、キーが文字列で値が `T` 型のオブジェクトを表現できます。

しかし、型定義の意図を明確にしたい場合は、Record を使う方が、「キーの型」と「値の型」が明確に分離されていて分かりやすいと感じる人も多いでしょう。

—

4. どっちを選ぶ?使い分けのポイントまとめ

さて、ここまでインデックスシグネチャと Record の使い方を見てきました。では、具体的にどのような状況でどちらを選ぶべきでしょうか?

以下のポイントを参考にしてみてください。

| 特徴/機能 | インデックスシグネチャ `[key: string]: T` | Record |
| :—————– | :————————————— | :————————————————— |
| 主な用途 | 包括的・全般的な型定義 | 特定のキー集合に対する型定義 |
| キーの指定 | `string` (または `number`, `symbol`) | `string`, `number`, `symbol` またはそれらのユニオン型/リテラル型 |
| 表現力 | 柔軟性が高い | より具体的で、キーを制限できる |
| 固定プロパティ | 併用可能(型が包含されている必要あり) | 併用は直接的ではない(Mapped Typeで実現可能) |
| コードの可読性 | シンプル | キーと値の関係が明示的で分かりやすい |
| 例 | 汎用的な設定オブジェクト、APIレスポンス | 定数リスト、状態管理のキー、特定属性の定義 |

迷ったときの「鉄則」

  • 「このオブジェクトは、どんな文字列キーでも、値は〇〇型」 という、緩やかな包括的な定義をしたいなら → インデックスシグネチャ
  • 「このオブジェクトのキーは、これとこれとこれ(または、この中から選ばれたもの)で、値は〇〇型」 という、より限定的で具体的な定義をしたいなら → Record

特に、許可するキーを明確にしたいという意図が強い場合は、Record を使うのがおすすめです。`K` にユニオン型を指定することで、意図しないキーの追加を防ぐことができます。

例えば、以下のようなケースです。

  • APIレスポンスで、キーは固定ではないが、値はすべて文字列である場合:
  • `[key: string]: string;` (インデックスシグネチャ)
  • `Record;` (Record)

どちらでも良いですが、後者の方が「キーが文字列、値が文字列」という関係が明確です。

  • アプリケーションのテーマ設定で、キーは `light` または `dark` のみ、値はオブジェクトである場合:
  • `Record<'light' | 'dark', ThemeObject>;` (Record)

この場合、インデックスシグネチャではキーを `’light’` と `’dark’` に限定することができないため、Record が最適です。

—

5. より発展的な使い道:Mapped Typesとの組み合わせ

インデックスシグネチャと Record の話は、TypeScriptのMapped Types(マッピングされた型)という、さらに強力な機能と密接に関連しています。

Mapped Typesは、既存の型から新しい型を生成する仕組みで、Record もその一種と見なすことができます。

例えば、あるオブジェクトのすべてのプロパティを読み取り専用にしたい、といった場合に Mapped Types が活躍します。

interface OriginalConfig {
timeout: number;
retries: number;
logLevel: ‘debug’ | ‘info’ | ‘error’;
}

// Mapped Type を使って、すべてのプロパティを readonly にする
type ReadonlyConfig = {
readonly [P in keyof OriginalConfig]: OriginalConfig[P];
};

// 上記は、以下のように書くのとほぼ同等です(より簡潔)
// type ReadonlyConfig = Readonly;

const config: ReadonlyConfig = {
timeout: 5000,
retries: 3,
logLevel: ‘info’
};

// config.timeout = 10000; // Error: Cannot assign to ‘timeout’ because it is a read-only property.

このように、Mapped Types を理解すると、TypeScriptでより柔軟で強力な型定義が可能になります。インデックスシグネチャや Record は、その Mapped Types の世界への入り口とも言えるでしょう。

—

まとめ:TypeScriptで型安全なオブジェクト定義をマスターしよう!

今日の記事では、「キーが不定なオブジェクト」を型安全に扱うための2つの強力な方法、インデックスシグネチャとRecord について、その使い方から、違い、そして使い分けのポイントまでをじっくりと解説しました。

  • インデックスシグネチャは、`[key: string]: T` のように、包括的で緩やかな型定義に適しています。
  • Record は、`Record` のように汎用的な使い方もできますが、特に `K` にユニオン型などを指定することで、許可するキーを限定した、より具体的な型定義に威力を発揮します。

どちらも、JavaScriptの動的なオブジェクトの特性をTypeScriptで型安全に扱うための重要なツールです。この記事で学んだ知識を活かして、皆さんのコードの安全性と可読性をさらに高めていってくださいね。

これらの基礎をしっかりとマスターすれば、TypeScriptでの開発はさらに楽しく、そして自信を持って進められるはずです。

もし、「こんなケースではどうすればいい?」といった疑問があれば、ぜひコメントで教えてください。皆さんのTypeScriptライフを、これからも応援しています!

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