キーが不定なオブジェクトを型安全に扱う:インデックスシグネチャ 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
Record
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
// 特定のキーのみを許可する例
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
// number のリテラル型
type ErrorCodes = 400 | 404 | 500;
type ErrorMessages = Record
2. Record とインデックスシグネチャの基本的な使い分け
- インデックスシグネチャ `[key: string]: T`:
- 「どんな文字列キーでも、値は `T` 型」 という、包括的で緩やかな制約を定義したい場合に適しています。
- キーの候補が非常に多い、あるいは未知数である場合に便利です。
- Record
- 「キーが `K` の型で、値が `V` 型」 という、より具体的なキーと値のペアの関係を定義したい場合に強力です。
- 特に、`K` にユニオン型やリテラル型を指定することで、許可されるキーを限定したい場合に非常に有効です。
3. Record はインデックスシグネチャ `[key: string]: T` とほぼ同等
`Record
しかし、型定義の意図を明確にしたい場合は、Record
—
4. どっちを選ぶ?使い分けのポイントまとめ
さて、ここまでインデックスシグネチャと Record
以下のポイントを参考にしてみてください。
| 特徴/機能 | インデックスシグネチャ `[key: string]: T` | Record
| :—————– | :————————————— | :————————————————— |
| 主な用途 | 包括的・全般的な型定義 | 特定のキー集合に対する型定義 |
| キーの指定 | `string` (または `number`, `symbol`) | `string`, `number`, `symbol` またはそれらのユニオン型/リテラル型 |
| 表現力 | 柔軟性が高い | より具体的で、キーを制限できる |
| 固定プロパティ | 併用可能(型が包含されている必要あり) | 併用は直接的ではない(Mapped Typeで実現可能) |
| コードの可読性 | シンプル | キーと値の関係が明示的で分かりやすい |
| 例 | 汎用的な設定オブジェクト、APIレスポンス | 定数リスト、状態管理のキー、特定属性の定義 |
迷ったときの「鉄則」
- 「このオブジェクトは、どんな文字列キーでも、値は〇〇型」 という、緩やかな包括的な定義をしたいなら → インデックスシグネチャ
- 「このオブジェクトのキーは、これとこれとこれ(または、この中から選ばれたもの)で、値は〇〇型」 という、より限定的で具体的な定義をしたいなら → Record
特に、許可するキーを明確にしたいという意図が強い場合は、Record
例えば、以下のようなケースです。
- APIレスポンスで、キーは固定ではないが、値はすべて文字列である場合:
- `[key: string]: string;` (インデックスシグネチャ)
- `Record
;` (Record)
どちらでも良いですが、後者の方が「キーが文字列、値が文字列」という関係が明確です。
- アプリケーションのテーマ設定で、キーは `light` または `dark` のみ、値はオブジェクトである場合:
- `Record<'light' | 'dark', ThemeObject>;` (Record)
この場合、インデックスシグネチャではキーを `’light’` と `’dark’` に限定することができないため、Record
—
5. より発展的な使い道:Mapped Typesとの組み合わせ
インデックスシグネチャと Record
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
—
まとめ:TypeScriptで型安全なオブジェクト定義をマスターしよう!
今日の記事では、「キーが不定なオブジェクト」を型安全に扱うための2つの強力な方法、インデックスシグネチャとRecord
- インデックスシグネチャは、`[key: string]: T` のように、包括的で緩やかな型定義に適しています。
- Record
は、`Record ` のように汎用的な使い方もできますが、特に `K` にユニオン型などを指定することで、許可するキーを限定した、より具体的な型定義に威力を発揮します。
どちらも、JavaScriptの動的なオブジェクトの特性をTypeScriptで型安全に扱うための重要なツールです。この記事で学んだ知識を活かして、皆さんのコードの安全性と可読性をさらに高めていってくださいね。
これらの基礎をしっかりとマスターすれば、TypeScriptでの開発はさらに楽しく、そして自信を持って進められるはずです。
もし、「こんなケースではどうすればいい?」といった疑問があれば、ぜひコメントで教えてください。皆さんのTypeScriptライフを、これからも応援しています!