Map Objectのイディオム集

Map Objectは JavaScriptで連想配列を実現するための機能です。
今まではObjectを連想配列として使っていたわけですが、ES6から、専用のクラスが用意されるようになりました。

今まであまり使ってこなかったですが、せっかくなので、よく使う表現をメモしておきます

使い方はざっと以下のような感じ、

```.ts
const user1: User = {
 userId: 1,
 userName: "user1"
}

const user2: User = {
 userId: 2,
 userName: "user2"
}

const userMap = new Map<number, User>() // Map Objectを作成。キーとバリューになる型をジェネリックを使って定義できる。また、定義しなくても型推論してくれる

userMap.set(user1.userId, user1) // UserオブジェクトをMapに登録
userMap.set(user2.userId, user2)

userMap.get(1) // userIdが1のuserを取得
userMap.delete(2) // userIdが2のuserを削除
```

### 基本

上で紹介したもの以外に以下のようなメソッドが用意されています。

##### `.has(key)`

そのキーのメンバが存在するかどうかをチェックします。

##### `.entries()` / `.keys()` / `.values()`

それぞれ、Mapのメンバ、キー、バリューのイテレータを返します。
単体で使うことは少ないですが、後述する配列への変換処理だったり、マージ処理するときに活躍します。

##### `.forEach()`

Arrayで使えるforEachと同じような感じですね。Mapオブジェクトのメンバーを回しながら順番に処理してくれます。

``` .js
userMap.forEach((val,key,self)=>{
    console.log(key, val,self)
})

```

##### `.size`

Arrayで言うところの`.length`ですね。Map Objectに入っている要素数を返してくれます。

```.ts
userMap.size // => 2
```

### 配列への変換

Map Objectは便利なのですが、いかんせん繰り返し処理に使えるメソッドが`forEach`しかなく、`map()`や`filter()` `reduce()` などが使えるArrayが恋しくなるときが多々あります。

以下のように書けば、Map ObjectをArrayに変換することができます。

```.ts
const userArray = [...userMap.values()]    // => [user1, user2 ....]
```

MDNのサイトで紹介されているのは以下のような方法ですね。
こちらでも同様です

```.ts
const userArray = Array.from(userMap.values())    // => [user1, user2 ....]
```

もしもkeyだけの配列がほしいのであれば以下のような感じです

```.ts
const userArray = [...userMap.keys()]    // => [1, 2 ....]
```


### 配列からMap Objectへの変換

なにか処理をするためにArrayに変換するのであれば、処理が終わったあとはもう一度 Map Objectに戻したくなるでしょう。
もしくはAPIから取得した値は配列で帰ってくるかもしれません。
その場合は以下のように書くことでMap Objectに変換することができます。
Map Object → Arrayのときよりも若干面倒ですね

```.ts
const userMap = new Map(userArray.map(user => [user.userId, user])) 
// => Map<number,User>へ変換
```

### Map Object同士をマージする

配列ではなくMap Objectを使う主な理由は、キーを使った値へのアクセスが容易(かつ高速)なことに加えて、常にキーが一意になるようにObjectの塊を取り扱うことができることです。

例えば、同じユーザーIDを持つUserが存在する可能性のある2つの配列（`usersArray1`と`usersArray2`）を一つに結合して、ユーザーIDが一意になるように重複を取り除く事を考えてみましょう。

配列に格納されている値がプリミティブな値であれば、`Set`を用いて一意な配列を作る事はできます。
しかし今回扱いたいObjectの場合は、以下のようにうまく一意になってくれません。

```.ts
const user2: User = {
    userId: 2,
    userName: "user2"
}

const sameUser2: User = {
    userId: 2,
    userName: "user2"
}

const duplicateUserArray = [user2, sameUser2]

// user2とsameUser2は、全く同じメンバーを持っているが、インスタンスが違うので、重複しているとはみなされない
console.log([...(new Set(duplicateUserArray))]) 
// => [ { userId: 2, userName: 'user2' }, { userId: 2, userName: 'user2' } ]
```

しかし、こんな時もMap Objectであれば解決です。以下のように書くことで、キーが重複した値を除去しつつ、2つのMap Objectをマージすることができます。
重複したキーが有る場合は、後に指定したMap（この場合は`userMap2`）が優先されることに注意してください。

```.ts
// userMap1とuserMap2をマージ
const mergeUserMap = new Map([...userMap1.entries(),...userMap2.entries()])
```

```例.ts
const userMap1 = new Map()  // Map Objectを作成
userMap1.set(user1.userId, user1)  // user1を追加
userMap1.set(user2.userId, user2)  // user2を追加

const userMap2 = new Map()  // Map Objectを作成
userMap2.set(user1.userId, user1)  // user1を追加
userMap2.set(user3.userId, user3)  // user3を追加

// userMap1とuserMap2をマージ
const mergeUserMap = new Map([...userMap1.entries(),...userMap2.entries()])
// => Map {
//    1 => { userId: 1, userName: 'user1' },
//    2 => { userId: 2, userName: 'user2' },
//    3 => { userId: 3, userName: 'user3' } }
```

Objectで言うところの`Object.assign()`と同じような動きですね

```.ts
// userObjectとuserObject2をマージ
const mergeUserObject = Object.assign(userObject1, userObject2)
```

### キーの差し替え

あまり頻度は多くないですが、Map Objectに設定したキーを別の値に置き換えたMap Objectを作りたくなるときがあります。
そんなときは以下のようにかけます。

```.ts
// キーをuserIdからuserNameに変更
const nameMap = new Map([...userMap.values()].map(user => [user.userName, user]))
```

一度配列に変換してからMapに戻すというめんどくさいことをやっているのであまり良くないですね。ちなみに一意じゃない値をキーに設定すると、例によって重複削除されてしまうので注意してください。

### オブジェクトへの変換

**2019/05/24追記： [コメント](https://qiita.com/KuwabataK/items/ca5029995b06d5caf899#comment-552ef766c8cac854fb55)で、より良い書き方を頂きました。感謝!**

[`Object.fromEntries()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries)を使う方法です。今はStage4の状態みたいですが、IE、Edge,Node.js以外の主要な環境では使えるみたいです。

TypeScript側での対応はまだみたいですが、[2019/05/24時点でmasterにマージされている](https://github.com/microsoft/TypeScript/blob/master/src/lib/es2019.object.d.ts)ようなので、次のリリースでは使えるようになるのではないかと思います。

```.ts
const userObject = Object.fromEntries(userMap)  // userMapをuserObjectに変換
```

<details><summary>以前のやり方。`Object.fromEntries()`が使えるなら読む必要はない</summary><div>

　

~~`JSON.stringify()`したいときなど、どうしてもObjectにしたいときがあります。
そういうときは以下のコードでオブジェクトに変換することができます。~~

~~本当は一文で書きたかったんですが、難しくなりすぎるのでやめました。~~

```.ts
const userObject:any = {}
userMap.forEach((u,uId)=>{
    userObject[uId] = u
})
```

~~いっそ関数化してしまっても良いかもしれません。~~

```.ts

/**
 * Map => Objectに変換する
 */
function mapToObject(map: Map<any,any>):any{
    const object: any = {}
    map.forEach((val, key) => {
        userObject[key] = val
    })
    return object
}

```

[参考](https://qiita.com/mktoho-n/items/6b96674c9c7ba542d2be)

</div></details>

### オブジェクトからMap Objectへの変換

**2019/05/24追記: [コメント](https://qiita.com/KuwabataK/items/ca5029995b06d5caf899#comment-552ef766c8cac854fb55)でより良い書き方を頂きました。感謝!**

`Object.entries()`を使うことでObject→Map Objectへ変換することができます。

```.ts
const userMap = new Map(Object.entries(userObject)); // userObjectをuserMapに変換
```

ただし Symbol のキーは処理の過程で除かれてしまうので、それも含めるなら、以下のように書きます

```.ts
const userMap = new Map()
for (const key of [...Object.keys(userObject), ...Object.getOwnPropertySymbols(userObject)]) {
  userMap.set(key, userObject[key])
}
```

以上、

~~後半考えると、別にObjectでもいいのでは？とも思っちゃいますね。ただ、配列→オブジェクトへの変換はMapのほうが楽かなという印象があります。全体的に、Array FriendlyなObjectって印象ですね。~~

Object ⇔ Map Objectの変換も簡単にかけるので、連想配列にMap Objectを使わない理由はなさそうです。
これからはもっと使っていきたいと思います。

もっとこんな便利な書き方あるよって人がいれば教えてください