Map<K,V> から [K, V][] （タプルの配列）に変換する

この記事は、 @rithmety さんのコメントで速攻解決します。
よって、下記の記事は「こいつ、なんだかぁ〜」レベルになってます。（笑）

## ことの発端

ちょっと現実逃避をしたくて、ウェブサーフィン（死語）していたところ`Scala.js`があることを知りました。

https://www.scala-js.org/

このページには「今までこう書いてたけど、`Scala.js`ならこう書ける」っていう比較がありまして。。。

```typescript:TypeScript
class Person {
  constructor(public firstName: string,
      public lastName: string) {
  }

  fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

const personMap = new Map<number, Person>([
  [10, new Person("Roger", "Moore")],
  [20, new Person("James", "Bond")]
]);

const names = new Array<string>();
for (const [key, person] of personMap) {
  if (key > 15) {
    names.push(`${key} = ${person.firstName}`);
  }
}
```

```scala:Scala.jsだとこんなにシンプル！
class Person(val firstName: String, val lastName: String) {
  def fullName(): String =
    s"$firstName $lastName"
}
val personMap = Map(
  10 -> new Person("Roger", "Moore"),
  20 -> new Person("James", "Bond")
)
val names = for {
  (key, person) <- personMap
  if key > 15
} yield s"$key = ${person.firstName}"
```

いや〜、`Scala.jp`を候補としている人が、今どき`TypeScript`でこんな書き方しないでしょと思い、リファクタリングをしてみました。

```typescript
const name = Object.keys(personMap)// こいつが string[] になりやがるんだよね～～
  .map(key => parseInt(key)) // あ。。。　う〜〜ん。。。
  .filter(key => key > 15)
  .map(key => `${key} = ${personMap.get(key)}`); // personMap.get(key) は string | undefined　なんだよなぁ。。。
```

あ〜、そうそう!
そうだったわ。。。
`TypeScript`のイケてないところを見事に突いた絶妙なサンプルだな。
特に連想配列のキーを`number`にするところなんか悪意しか感じないｗ
（↑　後段で、そういう問題じゃないということに気付きます）
まぁ、それはそうと、これって何とかならないかな？


## で、どうする？

`Map<number, Person>` を `[number, Person][]` に変換できれば、メソッドチェーンで色々便利そう。


## 書いてみる

メタプログラミングは久々なのでリハビリがてら書いてみるとする。

```typescript
function mapToTuples<
  T extends Map<unknown, unknown>,
  K = T extends Map<infer K, unknown> ? K : never,
  V = T extends Map<unknown, infer V> ? V : never
>(map: T): [K, V][] {
  return Object.keys(map).map(idx => [idx as K, map.get(idx) as V]);
}
```

意外とアッサリ書けた。
で、こいつを使って書き直してみる。

```typescript
const names = mapToTuples(personMap)
  .filter(x => x[0] > 15)
  .map(x => `${x[0]} = ${x[1].firstName}`)
```

`Scaler.js` よりスッキリしていて素敵～（小並感）
まぁ、`mapToTuples()` は、内部で配列作っるのでメモリのコストを考えると微妙だけど、無理矢理メソッドチェーンで何とか頑張るよりは可読性は高いんじゃないかしら？

と、これで終わりにしようと思って、いざ実行してみると、`names` が `[]` 空っぽじゃないですか！！

## 何故 mapToTuples() がダメなのか

`Object` と `Map` の違いは下記のとおり。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map#object_%E3%81%A8_map_%E3%81%AE%E6%AF%94%E8%BC%83

つまり、`Map`オブジェクトに対して `Object.keys()` は使えません。
う～ん、何となく過去コードでバグ作ってないかとモヤモヤする。。。

## 書き直す

簡単に目的の`[K, V][]`にしても良かったのだけど、`map()` という高階関数を持つオブジェクトを返却しました。
（ジェネリクスのリハビリ中なので寄り道気味です）

```typescript
function mapTransform<
  T extends Map<unknown, unknown>,
  K = T extends Map<infer K, unknown> ? K : never,
  V = T extends Map<unknown, infer V> ? V : never
>(src: T): {
    map<
        MAP_FN extends (kv: [K, V]) => unknown,
        MAP_FN_RET = MAP_FN extends (kv: [K, V]) => infer R ? R : never
    >(f: MAP_FN): MAP_FN_RET[]
}{
  return {
    map<
        MAP_FN extends (kv: [K, V]) => unknown,
        MAP_FN_RET = MAP_FN extends (kv: [K, V]) => infer R ? R : never
    >(f: MAP_FN): MAP_FN_RET[] {
        const re = [] as MAP_FN_RET[];
        src.forEach((v, k) => re.push(f([k, v] as [K, V]) as MAP_FN_RET));
        return re;
    }
  }
}
```

んで、これを使うと、こんな感じになります。

```typescript
const names = mapTransform(personMap)
    .map(x => x)
    .filter(x => x[0] > 15)
    .map(x => `${x[0]} = ${x[1].firstName}`);
```

う～ん、やっぱり`mapTransform()`は素直に `[K, V][]` を返却しても良かったかな？（苦笑）
なので、再度`mapToTuples()`という関数名に変更して再実装。

```typescript:配列を返却することにした（関数名も変更）
function mapToTuples<
  T extends Map<unknown, unknown>,
  K = T extends Map<infer K, unknown> ? K : never,
  V = T extends Map<unknown, infer V> ? V : never
>(src: T): [K, V][]
{
    const re = [] as [K, V][];
    src.forEach((v, k) => re.push([k, v] as [K, V]));
    return re;
}
```

これなら、こう書けるか。。。

```typescript
const names = mapToTuples(personMap)
    .filter(x => x[0] > 15)
    .map(x => `${x[0]} = ${x[1].firstName}`);
```


## ついでに Object の変換も作っておく

```typescript
function objectToTuples<
  T extends {[_ in string | number | symbol]: unknown},
  K = T extends {[_ in infer K]: unknown} ? K : never,
  V = T extends {[_ in string | number | symbol]: infer V} ? V : never
>(src: T): [K, V][] {
    return Object.keys(src).map(k => [k as K, src[k] as V]);
}
```

んで、こんな感じで使う。

```typescript
const obj = {
    1: "one",
    2: "two"
};
const x = objectToTuples(obj); // x は [1 | 2, string][]
```

一見、良さげに見えるんだけど、タプルの１個めの型は `1 | 2` と認識しているんだけど、実体は文字列が入っているという罠があります。
原因は`Object.keys()`が`string[]`を返却しやがる。。。
というか、`Object`のプロパティは`string`か`symbol`のみなので、しょうがないです。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Data_structures#%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88

で、更に`symbol`をキーとしている場合、`Object.keys()` や `Object.entries()` では列挙されません。

```typescript
const s = Symbol();
const o = {
  1: "one",
  0o10: "eight",
  0xA: "ten",
  [s]: "sym",
  s: "str"
};
for (const [key, value] of Object.entries(o)) {
  console.log(`${key}:${typeof key} -> ${value}`);
}
console.log(`symbol ${o[s]}`);
```

```:結果
1:string -> one
8:string -> eight
10:string -> ten
s:string -> str
symbol sym
```

ということで、下記のように`objectToTuples`書き直します。

```typescript
function objectToTuples<
  T extends {[_ in string | number /*| symbol*/]: unknown},
  V = T extends {[_ in string | number /*| symbol*/]: infer V} ? V : never
>(src: T): [string, V][] {
    return Object.keys(src).map(k => [k, src[k] as V]);
}
```

```typescript
const s = Symbol();
const o = {
  1: "one",
  0o10: "eight",
  0xA: "ten",
  [s]: "sym",
  s: "str"
};
const x = objectToTuples(obj); // x は [string, string][]
console.log(x)
```

あれ？
エラーにならない。。。orz

```:結果
[["1", "one"], ["8", "eight"], ["10", "ten"], ["s", "str"]]
```

`symbol`は暗黙の文字列変換しないが、これは`JavaScript`の仕様。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Symbol#%E3%82%B7%E3%83%B3%E3%83%9C%E3%83%AB%E3%81%AF%E6%96%87%E5%AD%97%E5%88%97%E3%81%AB%E3%80%8C%E8%87%AA%E5%8B%95%E5%A4%89%E6%8F%9B%E3%80%8D%E3%81%95%E3%82%8C%E3%81%AA%E3%81%84

ということは、`TypeScript`の型情報としての`symbol`の取り扱いを調べないとダメだな。。。（続くかも）