TypeScriptにおいて、データをキーと値のペアで管理するための手段として、古くからオブジェクト(Object)が利用されてきました。
しかし、大規模なアプリケーション開発や、より厳密な型安全性が求められる現代のプログラミングシーンでは、標準の組み込みオブジェクトであるMap型が非常に重要な役割を担っています。
Map型は、単なるキーと値の保持に留まらず、オブジェクトにはない優れた特性を数多く備えています。
この記事では、TypeScriptにおけるMap型の基礎から、オブジェクトとの使い分け、そして実務で役立つ応用テクニックまでを詳しく解説します。
TypeScriptにおけるMap型の基本定義と宣言
TypeScriptでMap型を使用するには、ジェネリクスを用いて「キーの型」と「値の型」を明示的に指定します。
基本的な宣言方法は、new Map<K, V>()の形式をとります。
// キーがstring型、値がnumber型のMapを定義
const userScores = new Map<string, number>();
// 初期値を指定して作成する
const settings = new Map<string, boolean>([
("notifications", true),
("darkMode", false)
]);
このように定義することで、指定した型以外のデータが混入することをコンパイル時に防ぐことが可能となります。
JavaScriptの標準機能としてのMapに加え、TypeScriptの強力な型推論と型チェックが組み合わさることで、ランタイムエラーのリスクを大幅に軽減できます。
Map型とObject型の決定的な違い
TypeScriptを利用する際、データを管理するためにObject型とMap型のどちらを選ぶべきか迷う場面は少なくありません。
両者の違いを正確に理解することは、コードの品質とパフォーマンスを最適化するための第一歩です。
キーとして利用できる型の自由度
Object型の場合、キーとして利用できるのはstring型、number型、またはsymbol型に限定されます。
一方で、Map型はあらゆる型の値をキーとして使用できるという大きな特徴を持っています。
// オブジェクトをキーにする例
const sessionMap = new Map<object, string>();
const user = { id: 1, name: "Tanaka" };
sessionMap.set(user, "active_session_token");
console.log(sessionMap.get(user));
"active_session_token"
この特性により、関数や複雑なクラスのインスタンスをキーにして、それに関連するメタデータを管理するといった高度な実装が容易になります。
データの順序と反復処理の挙動
Object型は、プロパティの順序が仕様上完全に保証されているわけではなく、特に数値のようなキーが含まれる場合に意図しない順序で処理されることがあります。
対してMap型は、要素が追加された順序を厳密に保持することが保証されています。
データの追加順に画面表示を行う場合や、キューのような順序性が重要なロジックを組む場合には、Map型が最適です。
パフォーマンスの優位性
頻繁に要素を追加したり削除したりするシナリオにおいて、Map型はObject型よりも優れたパフォーマンスを発揮するように設計されています。
特に要素数が膨大になる場合、Map型の delete メソッドや set メソッドは非常に効率的です。
また、要素の総数を取得する際、Object型では Object.keys().length を使って配列を一度生成する必要がありますが、Map型なら size プロパティを参照するだけで済みます。
| 特徴 | Object型 | Map型 |
|---|---|---|
| キーの型 | string, number, symbolのみ | 任意の型(オブジェクトや関数も可) |
| 要素の順序 | 完全には保証されない | 挿入順が保証される |
| 要素数の取得 | Object.keys()などを経由 | sizeプロパティで直接取得可能 |
| パフォーマンス | 頻繁な追加・削除には不向き | 頻繁な追加・削除に最適化されている |
Map型を使いこなすための主要メソッド
Map型には、データを直感的に操作するためのメソッドが豊富に用意されています。
ここでは、実務で頻繁に使用する基本的な操作方法を確認しましょう。
データの登録と取得
データの登録には set() を使い、取得には get() を使用します。
const productStock = new Map<number, number>();
// データの追加
productStock.set(101, 50);
productStock.set(102, 0);
// データの取得
const stock = productStock.get(101);
console.log(stock);
50
注意点として、get() メソッドはキーが存在しない場合に undefined を返します。
TypeScriptの strictNullChecks が有効な場合、戻り値の型は V | undefined となるため、適切なチェックが必要です。
存在確認と削除
特定のキーが含まれているかを確認するには has() を、削除するには delete() を利用します。
if (productStock.has(102)) {
console.log("商品ID:102 の在庫データが存在します");
}
// データの削除
productStock.delete(102);
console.log(productStock.has(102));
"商品ID:102 の在庫データが存在します"
false
Map全体を空にするには clear() メソッドを一回呼び出すだけで完了します。
実践的な反復処理のテクニック
Map型の要素をループで処理する方法はいくつかありますが、用途に応じて使い分けることが重要です。
for…of文による分割代入
Mapはイテレータを実装しているため、for...of 文で直接ループさせることができます。
このとき、配列の分割代入を利用すると、キーと値を個別の変数として簡潔に扱うことが可能です。
const priceList = new Map<string, number>([
("Apple", 150),
("Banana", 100),
("Orange", 120)
]);
for (const [name, price] of priceList) {
console.log(`${name}は${price}円です`);
}
"Appleは150円です"
"Bananaは100円です"
"Orangeは120円です"
keys()、values()、entries()の活用
特定の要素のみを抽出してループさせたい場合は、それぞれの専用メソッドを使用します。
keys(): すべてのキーを列挙するイテレータを返すvalues(): すべての値を列挙するイテレータを返すentries(): [キー, 値] のペアを列挙するイテレータを返す(デフォルトの挙動)
これらは配列ではないため、map() や filter() を直接使いたい場合は Array.from() やスプレッド構文で配列に変換する必要があります。
TypeScriptにおけるMap型の応用パターン
基本操作を理解したところで、実際の開発現場で遭遇する高度な活用方法を見ていきましょう。
オブジェクトをキーとしたキャッシュ(メモ化)
計算コストの高い処理の結果を、関数の引数(オブジェクト)をキーにして保存しておくケースです。
Object型ではオブジェクトをキーにできないため、ここでMap型の真価が発揮されます。
interface QueryConfig {
endpoint: string;
params: Record<string, string>;
}
const apiCache = new Map<QueryConfig, string>();
function fetchData(config: QueryConfig): string {
if (apiCache.has(config)) {
return apiCache.get(config)!;
}
// 重い処理のシミュレーション
const result = `Data from ${config.endpoint}`;
apiCache.set(config, result);
return result;
}
このように、参照透過性を利用したデータ管理にはMap型が不可欠です。
ReadonlyMapによるイミュータブルな設計
TypeScriptには、Mapの値を変更できないように制限する ReadonlyMap 型が用意されています。
不用意なデータの書き換えを防止し、堅牢なアーキテクチャを実現するために有効です。
const configMap: ReadonlyMap<string, string> = new Map([
("env", "production"),
("version", "1.5.0")
]);
// 以下はコンパイルエラーになります
// configMap.set("env", "development");
外部から設定値を渡す際や、グローバルな定数を定義する際に ReadonlyMap を活用することで、意図しない副作用を排除できます。
プロトタイプ汚染攻撃への対策
JavaScriptのObject型は、__proto__ などの特殊なキーを介して、プロトタイプチェーンが操作される「プロトタイプ汚染」の脆弱性が発生するリスクがあります。
Map型は継承したプロパティと格納したデータを混同しないため、セキュリティ面でも安全です。
外部のユーザー入力をそのままキーとして扱うような処理では、Object型ではなくMap型を採用することを強く推奨します。
Map型を使用する際の注意点
非常に便利なMap型ですが、いくつか意識しておくべきポイントがあります。
JSONへのシリアライズ
標準の JSON.stringify() はMap型をサポートしていません。
Mapをそのままシリアライズしようとすると空のオブジェクト {} になってしまいます。
const myMap = new Map([("id", 1)]);
console.log(JSON.stringify(myMap)); // "{}" と表示される
通信などでJSON形式にする必要がある場合は、一度オブジェクトに変換するか、配列に変換する処理を挟む必要があります。
キーの同一性判定(SameValueZero)
Mapがキーの同一性を判定する際には「SameValueZero」というアルゴリズムが使われます。
これは ===(厳密等価)とほぼ同じですが、NaN を NaN と等しいとみなす点が異なります。
しかし、オブジェクトをキーにする場合は「参照」が一致している必要がある点に注意してください。
内容が全く同じオブジェクトであっても、インスタンス(参照先)が異なれば、別のキーとして扱われます。
まとめ
TypeScriptのMap型は、柔軟なキー指定、順序の保証、高いパフォーマンス、そして安全な型定義を兼ね備えた非常に強力なデータ構造です。
従来のオブジェクトは、単純な静的設定やPOJO(Plain Old JavaScript Object)として利用し、動的なデータの追加や複雑なキー操作が必要な場面ではMap型を選択するという使い分けがベストプラクティスといえます。
特にTypeScriptのジェネリクスを活用することで、Map内のデータ構造を厳密に定義し、開発効率と保守性を同時に高めることが可能です。
今回紹介したテクニックを参考に、日々のコーディングにおいて適切なデータ構造の選択を行い、より堅牢なTypeScriptアプリケーションを構築していきましょう。
