TypeScriptにおいて、データをキーと値のペアで管理するための手段として、古くからオブジェクト(Object)が利用されてきました。

しかし、大規模なアプリケーション開発や、より厳密な型安全性が求められる現代のプログラミングシーンでは、標準の組み込みオブジェクトであるMap型が非常に重要な役割を担っています。

Map型は、単なるキーと値の保持に留まらず、オブジェクトにはない優れた特性を数多く備えています。

この記事では、TypeScriptにおけるMap型の基礎から、オブジェクトとの使い分け、そして実務で役立つ応用テクニックまでを詳しく解説します。

TypeScriptにおけるMap型の基本定義と宣言

TypeScriptでMap型を使用するには、ジェネリクスを用いて「キーの型」と「値の型」を明示的に指定します。

基本的な宣言方法は、new Map<K, V>()の形式をとります。

TypeScript
// キーが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型はあらゆる型の値をキーとして使用できるという大きな特徴を持っています。

TypeScript
// オブジェクトをキーにする例
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() を使用します。

TypeScript
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() を利用します。

TypeScript
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 文で直接ループさせることができます。

このとき、配列の分割代入を利用すると、キーと値を個別の変数として簡潔に扱うことが可能です。

TypeScript
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型の真価が発揮されます。

TypeScript
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 型が用意されています。

不用意なデータの書き換えを防止し、堅牢なアーキテクチャを実現するために有効です。

TypeScript
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をそのままシリアライズしようとすると空のオブジェクト {} になってしまいます。

TypeScript
const myMap = new Map([("id", 1)]);
console.log(JSON.stringify(myMap)); // "{}" と表示される

通信などでJSON形式にする必要がある場合は、一度オブジェクトに変換するか、配列に変換する処理を挟む必要があります。

キーの同一性判定(SameValueZero)

Mapがキーの同一性を判定する際には「SameValueZero」というアルゴリズムが使われます。

これは ===(厳密等価)とほぼ同じですが、NaNNaN と等しいとみなす点が異なります。

しかし、オブジェクトをキーにする場合は「参照」が一致している必要がある点に注意してください。

内容が全く同じオブジェクトであっても、インスタンス(参照先)が異なれば、別のキーとして扱われます。

まとめ

TypeScriptのMap型は、柔軟なキー指定、順序の保証、高いパフォーマンス、そして安全な型定義を兼ね備えた非常に強力なデータ構造です。

従来のオブジェクトは、単純な静的設定やPOJO(Plain Old JavaScript Object)として利用し、動的なデータの追加や複雑なキー操作が必要な場面ではMap型を選択するという使い分けがベストプラクティスといえます。

特にTypeScriptのジェネリクスを活用することで、Map内のデータ構造を厳密に定義し、開発効率と保守性を同時に高めることが可能です。

今回紹介したテクニックを参考に、日々のコーディングにおいて適切なデータ構造の選択を行い、より堅牢なTypeScriptアプリケーションを構築していきましょう。