TypeScriptには、辞書やハッシュマップ専用のクラスはありません。辞書(連想配列)は、インデックスシグネチャ { [key: string]: number } で型付けした普通のオブジェクト、同じ型を Record<string, number> と書いたもの、または Map<string, number> のどれかです。3つともキーで値を保持しますが、キーの型、存在しないキーの型、シリアライズの方法が違います。
文字列キーや JSON の形をしたデータには、Record<string, T> のオブジェクトを使うのが一般的です。文字列以外のキーや、エントリーを頻繁に追加・削除する場合は Map を使います。
インデックスシグネチャ
インデックスシグネチャ [key: KeyType]: ValueType は、「この型のどんなキーもこの型の値に対応する」という意味です。キーの名前(key、name、userId)は説明のためだけのものです。キーの型には string、number、symbol、テンプレートリテラルのパターン、またはそれらのユニオンを使えます。
JavaScriptは数値のキーを文字列に変換するので、{ [id: number]: string } のオブジェクトも実行時のキーは文字列です: Object.keys({ 1: "one" }) は [ '1' ] になります。数値のインデックスシグネチャが制限するのは、TypeScriptでのインデックスの書き方だけです。
Record<K, V>
Record<string, V> は { [key: string]: V } の省略形です。string の代わりにリテラルのキーのユニオンを使うと、すべてのキーを含まなければならない固定の辞書になります:
urls から staging を省くとコンパイルエラー TS2741(Property 'staging' is missing...)になるので、ユニオンのキーを持つ Record はチェック付きの対応表になります。Partial は各値を number | undefined にします。
ハッシュマップとしての Map
Map はどんな型のキーも受け付け、挿入順を保ち、size を持ち、存在しないキーを正直に型付けします: get は V | undefined を返します。
キーが存在するかチェックする
チェックの方法はいくつかあり、どれも同じ意味というわけではありません:
| チェック | 使える対象 | 注意点 |
|---|---|---|
Object.hasOwn(obj, key) | オブジェクト | ES2022。古い target では Object.prototype.hasOwnProperty.call(obj, key) を使う |
key in obj | オブジェクト | toString や constructor のような継承したキーでも true |
obj[key] !== undefined | オブジェクト | 存在しないキーと、undefined を保持しているキーを区別できない |
if (obj[key]) | オブジェクト | 値が 0、""、false の場合も false |
map.has(key) | Map | あとの map.get(key) を絞り込まない |
map.get(key) !== undefined | Map | オブジェクトと同じ undefined の注意点 |
継承したキーの問題があるため、"constructor" や "__proto__" のようなユーザーが入力したキーを扱う辞書に普通のオブジェクトを使うのは危険です。Map にはそうしたキーはありません。
存在しないキーの型の問題
インデックスシグネチャや Record<string, T> では、どのキーを読んでも型は T です。存在しないキーでもそうです。コンパイラーは、実行時には undefined である値のメソッドを呼ぶことを許してしまいます:
コンパイラーオプション noUncheckedIndexedAccess がこれを解決します。有効にすると colors["grass"] の型は string | undefined になり、チェックするまでは toUpperCase の呼び出しがコンパイルエラーになります。これは strict に含まれないので、tsconfig.json で別に有効にする必要があります。一緒に使うほかのフラグについては strict モード を参照してください。Map では get の型に必ず undefined が含まれるので、このような穴はありません。
追加、削除、ループ
delete はインデックスシグネチャのプロパティに使えます。オブジェクト型の必須の名前付きプロパティに使うとコンパイルエラー TS2790 The operand of a 'delete' operator must be optional. になります。
どれを使うか
| 必要なもの | 使うもの |
|---|---|
| 文字列キー、JSON の入出力 | Record<string, T> |
| 決まった既知のキーの集合で、すべて必須 | Record<"a" | "b", T> |
| 名前付きのプロパティに加えて任意のキー | インデックスシグネチャを持つオブジェクト型 |
| オブジェクト、数値のまま扱いたい数値、その他の文字列以外のキー | Map<K, V> |
| 頻繁な追加と削除、よく参照するサイズ | Map<K, V> |
| ユーザーから来るキー | Map<K, V>(継承したキーがない) |
よくある質問
TypeScriptで連想配列(辞書)を作るには?
普通のオブジェクトをインデックスシグネチャで型付けするか(const ages: { [name: string]: number } = {})、同じ意味の Record<string, number> を使います。エントリーは ages["ada"] = 36 のように追加します。キーが文字列でない場合や、追加と削除が頻繁なコレクションには new Map<string, number>() を使います。
TypeScriptに HashMap はありますか?
その名前ではありません。JavaScriptの組み込みの Map がハッシュマップです。Map<K, V> はキーと値のペアを保持してキーで高速に参照でき、挿入順を保ち、どんな型のキーも受け付けます。文字列キーなら Record<string, V> で型付けした普通のオブジェクトもよく使われます。
TypeScriptの連想配列でキーが存在するかチェックするには?
オブジェクトの辞書では Object.hasOwn(dict, key) か key in dict(toString のような継承したプロパティにも反応します)を使うか、値を読んで undefined と比べます。Map では map.has(key) を使うか、has はあとの get を絞り込まないので map.get(key) の結果を直接チェックします。
インデックスシグネチャと Record の違いは何ですか?
{ [key: string]: T } と Record<string, T> は同じ型を表します。Record のほうが短く、Record<"a" | "b", T> のように特定のキーのユニオンも受け取れ、その場合はすべてのキーが必須になります。インデックスシグネチャは、ひとつのオブジェクト型の中で名前付きのプロパティと組み合わせられ、{ [userId: string]: User } のようにキーに意味を示す名前を付けられます。
存在しないキーを読んでもエラーにならないのはなぜですか?
デフォルトでは、インデックスシグネチャや Record<string, T> に対する dict[key] の型は T です。実行時には存在しないキーの値は undefined なのにです。tsconfig.json で noUncheckedIndexedAccess を有効にすると型が T | undefined になり、チェックが必須になります。このオプションは strict には含まれません。