TypeScriptのMap:型付きのキーと値のコレクション
TypeScriptのMap
TypeScriptのMap:型付け、反復処理、保存 | We.Inc
TypeScriptでMap<K, V>に型を付け、ループし、JSONに変換して元に戻し、Map、Record、普通のオブジェクトのどれを使うかを判断しましょう。ブラウザーで動くMapも試せます。
TypeScriptのMapは、キーと値に型を付けた普通のJavaScriptのMapで、new Map<string, number>() のように書きます。キーにはどんな値でも使え、エントリーは最初に追加された順に返り、sizeでその数がわかり、getは V | undefined を返します。JSON.stringifyはMapをシリアライズしないので、先に変換しましょう。
Mapのメソッドと、TypeScriptが返す型
- 呼び出し
- 処理
- 得られる型(Map<string, number>)
- map.set(k, v)
- エントリーを追加または更新する
- Map<string, number>(チェーン可能)
- map.get(k)
- 値を読み取る
- number | undefined
- map.delete(k)
- エントリーを削除する
- 真偽値(存在していた場合はtrue)
- for (const [k, v] of map)
- 挿入順にループする
- k: string, v: number
下のプレイグラウンドは、ブラウザーで実際に動いているMapです。エントリーを追加、更新、削除しながら、3つの点に注目しましょう。エントリーが保つ順序、sizeが報告する値、そしてMapそのものを文字列化した場合と、変換したコピーを文字列化した場合の違いです。
- キーと数値を入力し、「セット」を押してそのエントリーを追加または更新します。
- 「削除」を押すとキー欄のキーが削除され、「クリア」を押すとMapが空になります。
- エントリーの一覧を読みましょう。これはMap自身の反復順序です。
- 2行のJSONを比べて、保存や送信の前にMapの変換が必要な理由を確認します。
- 空のMapを作るときは、ジェネリクスの引数を指定しましょう。new Map() だけだと Map<any, any> になり、後で読み取る値はすべて型を失います。
- 値を使う前に、has() で確認するか、get() の結果をundefinedと比較しましょう。非nullアサーションは、キーが本当に存在しないケースを隠してしまいます。
- オブジェクトのキーは参照で比較されます。同じフィールドを持つ2つのオブジェクトは別々のキーになるので、同じレコードを指したいならIDの文字列を使いましょう。
- MapをlocalStorageに保存するには、JSON.stringify([...map]) を保存し、new Map(JSON.parse(text)) で復元します。ペアの配列なら、文字列以外のキーも順序も保たれます。
よくある質問
TypeScriptでMapの型を宣言するには?
キーと値の型をジェネリクスの引数として渡します:const prices = new Map<string, number>()。フィールドや引数では型を Map<string, number> と書き、関数が読み取るだけなら ReadonlyMap<string, number> と書きます。
map.get() の型にundefinedが含まれるのはなぜですか?
ある時点でMapにどのキーが入っているかを、コンパイラーは知ることができないからです。getは V | undefined と型付けされているので、値をVとして扱う前に、if文のチェック、?? による代替値、has() による確認で絞り込みます。
JSON.stringify(map) が {} になるのはなぜですか?
Mapはエントリーをオブジェクトのプロパティではなく内部スロットに保持しているため、JSONからは空のオブジェクトに見えます。先に変換しましょう。キーが文字列ならObject.fromEntries(map)、どんな型のキーでも保てるペアの配列なら [...map] です。
MapとRecordのどちらを使うべきですか?
名前の決まった固定のキーの集まりや、JSONとやり取りするデータにはRecordを使いましょう。アプリの実行中にエントリーが増えたり減ったりする場合、キーが数値やオブジェクトの場合、挿入順が重要な場合はMapを使いましょう。
キーを更新すると末尾に移動しますか?
いいえ。既存のキーにセットすると値は変わりますが、元の位置は保たれます。末尾に移動したい場合は、キーを削除してからもう一度セットします。
Map、Record、普通のオブジェクト
- 普通のオブジェクトはObject.prototypeを継承するため、constructorのような名前が最初から存在し、キーは常に文字列かシンボルです。Mapは空の状態から始まり、どんな値もキーとして受け付け、エントリー数をsizeで報告します。
- Recordは型であって、入れ物ではありません。普通のオブジェクトを表すので、上で述べたオブジェクトの性質は実行時にもすべて当てはまります。Mapにはない、JSONへのネイティブな対応もその1つです。
キーの一致の仕組み
- MapはSameValueZeroのルールでキーを比較します。数値の1と文字列の "1" は別のキーで、NaNはNaNと一致し、オブジェクトは自分自身とだけ一致します。普通のオブジェクトなら、1は "1" に変換されて2つは同じキーになってしまいます。
- そのため、DOM要素、関数、リクエストオブジェクトをキーにした値のキャッシュには、Mapが自然な選択です。オブジェクトを使うと、文字列のIDをわざわざ作る必要があるケースです。
関数のシグネチャでMapに型を付ける
- 読み取るだけの関数では ReadonlyMap<K, V> を受け取りましょう。型からset、delete、clearが取り除かれるので、シグネチャそのものが、呼び出し側のMapは変更されないと伝えてくれます。
- ペアの配列からMapを作ると、TypeScriptはペアから型を推論します。配列が空になり得る場合や、別々に保ちたい型がペアに混在する場合は、ジェネリクスを明示的に指定しましょう。
型付きMapのレシピ
Mapで単語を数える
?? による代替値が、単語が初めて出てきた場合を処理します。だからこそ、getが number | undefined を返すのは煩わしいどころか役に立つのです。
MapをJSONとして保存し、読み込み直す
ペアの配列なら、すべてのエントリーとその順序が保たれます。すべてのキーが文字列なら、Object.fromEntriesが読みやすい選択です。
読み取り専用のMapを受け取る
total関数の中でprices.setを呼ぶとコンパイルエラーになるので、呼び出し側は防御的なコピーを作らずにMapを渡せます。
Mapのライブプレイグラウンド
キー
値(数値)
セット
削除
クリア
リセット
反復順のエントリー
Mapは空です。
size: {n}
"{v}" を追加しました。
"{v}" を削除しました。
"{v}" はMapにないため、deleteはfalseを返しました。
先にキーを入力してください。
値は数値でなければなりません。
無料で始める · クレジットカード不要
関連: ガイド|Webマーケティングと集客を学ぶ
製品
ご利用いただける方
機能
リソース
会社情報
サイトマップを見る