TypeScriptにおいて「辞書型(連想配列)」をどのように型定義し、実装するかはアプリケーションのパフォーマンスと型安全性に直結する重要なテーマです。
本記事は、TypeScriptにおけるdictionaryを実装する方法と選び方について解説します。
TypeScriptにおける「Dictionary(辞書型)」とは
プログラミングにおいて「Dictionary(辞書型)」とは、一意の「キー(Key)」とそれに紐づく「値(Value)」のペアを格納するデータ構造です。
連想配列(Associative Array)やハッシュマップ(Hash Map)と呼ばれることもあります。
ユーザーIDをキーにしてユーザー情報を検索したり、カテゴリ名をキーにして商品リストを分類したりと、Webフロントエンドおよびバックエンド開発において重要な内容になります。
しかし、TypeScriptでDictionaryを扱う場合、「単なるオブジェクト{}」に何でも詰め込むわけにはいきません。
コンパイラに「どんな型のキーで、どんな型の値が入るのか」を正確に伝える必要があります。
インデックスシグネチャによる基本のDictionary
TypeScriptの初期から存在するベーシックなDictionaryの定義方法が「インデックスシグネチャ(Index Signatures)」です。
- インデックスシグネチャの基本構文
- 柔軟性と引き換えになる型安全性の落とし穴
インデックスシグネチャの基本構文
「キーのプロパティ名は事前には分からないが、キーの型と値の型は決まっている」という場合に使用します。
角括弧[]を用いて定義します。
// キーが文字列(string)で、値が数値(number)であるDictionaryの定義
interface ScoreDictionary {
[key: string]: number;
}
const scores: ScoreDictionary = {
math: 90,
english: 85,
science: 95,
};
// 動的なキーでのアクセスや追加が可能
scores["history"] = 80;キーに指定できる型はstring、number、symbol、あるいはテンプレートリテラル型などに限られます。
柔軟性と引き換えになる型安全性の落とし穴
インデックスシグネチャは柔軟ですが、危険性も孕んでいます。
const mathScore = scores["math"]; // number型として推論される
const musicScore = scores["music"]; // これもnumber型として推論されるが、実際は undefined!デフォルトのTypeScript設定では、存在しないキー(”music”)にアクセスしてもエラーになりません。
コンパイラは「キーが文字列なら、必ず数値が返ってくる」と信じ込むため、予期せぬエラー(undefinedに対する操作)を引き起こす原因となります。
Record型を活用したDictionary
TypeScript開発において、インデックスシグネチャよりも用いられるのがユーティリティ型であるRecord<Keys, Type>です。
Record<K, T>の基本とメリット- Union型を用いたキーの厳格な制限
Record<K, T>の基本とメリット
Record型は、内部的にはインデックスシグネチャを用いたMapped Typesのシンタックスシュガー(簡略表現)です。
しかし、interfaceを宣言する手間が省けコードがスッキリします。
// インデックスシグネチャと同じ意味を持つRecord型の定義
type UserDictionary = Record<string, { name: string; age: number }>;
const users: UserDictionary = {
"u001": { name: "田中", age: 25 },
"u002": { name: "佐藤", age: 30 },
};Union型を用いたキーの厳格な制限
Record 型が威力を発揮するのは、キーの型に Union型(特定の文字列の集合) を指定した時です。
これにより、「許可されたキーしか持てない」かつ「指定したすべてのキーを網羅しなければならない」という制約を持ったDictionaryを作成できます。
type AppTheme = "light" | "dark" | "system";
// AppThemeに含まれる3つのキーすべてを定義しないとコンパイルエラーになる
const themeColors: Record<AppTheme, string> = {
light: "#FFFFFF",
dark: "#000000",
system: "auto",
};将来的にAppThemeに"high-contrast"というテーマが追加された際themeColorsの定義箇所でエラーが発生するため、対応漏れを防げます。
ES6Mapオブジェクトを用いたDictionary
オブジェクトリテラル{}をベースにしたDictionaryでしたが、JavaScript(ES6以降)には辞書用途に特化したMapクラスが用意されています。
- Mapオブジェクトの型定義と基本操作
- Object(Record/インデックスシグネチャ)との決定的な違い
Mapオブジェクトの型定義と基本操作
TypeScriptでMapを初期化する際、ジェネリクス<K, V>を用いてキーと値の型を明示します。
// キーがstring、値がUserオブジェクトであるMapの定義
interface User { id: string; name: string; }
const userMap = new Map<string, User>();
// 要素の追加 (set)
userMap.set("u001", { id: "u001", name: "Alice" });
// 要素の取得 (get) - 戻り値は User | undefined になるため安全!
const user = userMap.get("u001");
// 要素の存在確認 (has)
if (userMap.has("u001")) {
console.log("ユーザーが存在します");
}Object(Record/インデックスシグネチャ)との決定的な違い
なぜMapを使うのでしょうか?
通常のオブジェクトベースのDictionaryにはない、以下のメリットがあるからです。
- キーの型が自由
オブジェクトのキーは文字列かシンボルしか使えませんが、Mapはオブジェクトや関数、数値そのものをキーにできます。 - 順序の保証
Mapは要素が追加された順番を記憶しています。
ループ処理(for...of)を行う際、意図した順番でデータを取り出せます。 - パフォーマンス
要素を追加・削除する操作において、Mapは通常のオブジェクトよりも高速に動作するよう最適化されます。 - サイズ取得
Object.keys(obj).lengthのような重い処理を書かずとも、userMap.sizeで要素数を取得できます。
どの「TypeScript dictionary」実装を選ぶべきか?
これら3つの手法は、どのように使い分けるべきでしょうか。
以下の比較表と判定基準を参考にしてください。
| 特徴 | インデックスシグネチャ | Record<K, T> | Map<K, V> |
|---|---|---|---|
| キーの制約 | 柔軟(string 等) | 厳格(Union型で網羅性を強制可) | 柔軟(オブジェクトもキーにできる) |
| 記述の簡潔さ | 普通 | 非常に簡潔 | 冗長(set, getの呼び出しが必要) |
| JSONへの変換 | JSON.stringifyで一発 | JSON.stringifyで一発 | 一手間必要(Arrayへの変換等が必要) |
| 実行時パフォーマンス | 静的・小規模なら高速 | 静的・小規模なら高速 | 動的・大規模な追加/削除に最適 |
| 型の安全性(取得時) | 要注意(設定に依存) | 要注意(設定に依存) | 常に安全(undefined の可能性を型が示す) |
ユースケース別・最適なアプローチの判定基準
Record<K, T>を使うべきケース
状態管理、定数のマッピング、APIのレスポンス定義などほとんどのケースではRecord型が最適です。
コードが読みやすく、Union型と組み合わせた時の堅牢性が高いです。Map<K, V>を使うべきケース
キャッシュシステムの実装、DOM要素とデータの紐付け(キーがオブジェクトの場合)、数万件のデータの出し入れが発生するなど、パフォーマンスと順序性が求められる場面で採用します。- インデックスシグネチャを使うべきケース
Record型では複雑な型定義(特定のキーは固定で持ちつつ、それ以外の任意のキーも許可したいオブジェクト)を定義する場合などに使用します。
Dictionaryの型安全対策
オブジェクトベースのDictionaryを選択した場合、「存在しないキーへのアクセス」によるundefinedエラーを防ぐことがTypeScript開発の絶対条件です。
noUncheckedIndexedAccessの有効化Partialユーティリティ型との組み合わせ
noUncheckedIndexedAccessの有効化
tsconfig.jsonを変更できない場合や特定のDictionaryだけを安全に扱いたい場合は、Partial型とRecord型を組み合わせるのがベストプラクティスです。
type ProductId = "p1" | "p2" | "p3";
interface ProductInfo { name: string; price: number; }
// すべてのProductIdに対するデータが存在するとは限らない場合
// Record の外側を Partial で囲む
const inventory: Partial<Record<ProductId, ProductInfo>> = {
"p1": { name: "MacBook", price: 150000 }
};
const item = inventory["p2"]; // item は ProductInfo | undefined として推論されるPartialはすべてのプロパティをオプショナル(?)にするユーティリティ型です。
これにより、「キーは定義されているが、値は存在しないかもしれない(疎な配列)」というデータ構造を正確に型システムで表現できます。


コメント