Map.prototype.get の挙動を理解した

# TL;DR
- ECMASCriptのMapオブジェクトのgetによる比較操作は、「[SameValueZero](https://tc39.es/ecma262/#sec-samevaluezero)」で行われる
- 故に、TypeScriptでMapオブジェクトをkeyにクラスを指定して使用する時、Javaのようにequalsメソッドをオーバーライドをしても、Javaのようにvalueを取得することはできない
- keyにはクラスから得られる文字列などを指定して、valueを取得するなどの代替手段を取る必要がある

# Map.prototype.get の仕様を正しく理解していなかった
テスト駆動開発を学ぶべく、『[テスト駆動開発](https://shop.ohmsha.co.jp/shopdetail/000000004967/)』の第一部 多国通貨をなぞってました。
すると、Mapを使った為替レートの管理をしているロジックがあったんですね。本書はJavaで書かれていたので、TypeScriptで実装し直してみたら、一点本書と挙動が違う点がありました。

```typescript
class Bank {
  private rates: Map<Pair, number> = new Map();
  constructor() {}

  public addRate = (from: string, to: string, rate: number): void => {
    this.rates.set(new Pair(from, to), rate);
  };

  public rate = (from: string, to: string): number => {
    if (from === to) return 1;
    return this.rates.get(new Pair(from, to)) ?? 1;
  };
}

class Pair {
  constructor(private from: string, private to: string) {
    this.from = from;
    this.to = to;
  }

  equals(object: object) {
    const pair: Pair = object as Pair;
    return this.from === pair.from && this.to === pair.to;
  }
}

const bank: Bank = new Bank();
bank.addRate('CHF', 'USD', 2);
console.log(bank.rates('CHF', USD))
```

書籍では、最後の行のconsole.logで出力される値の期待値が、 `2` になる想定でしたが、実際は `1` でした。Bankクラスのrateメソッドでの挙動を確認すると、

```typescript
return this.rates.get(new Pair(from, to)) ?? 1;
```

の `this.rates.get(new Pair(from, to))` が `undefined` でした。

```typescript
bank.addRate('CHF', 'USD', 2);
```

でMapに登録して

```shell
console.log
  Map(1) {
    Pair { from: 'CHF', to: 'USD', generateKey: [Function (anonymous)] } => 2
  }
```

上記のようにMapにもちゃんとsetできていることが確認できました。

# Map.prototype.get の仕様
Mapにちゃんとkey, valueがsetできているのに、なぜgetできないのか調べると以下の記事を見つけました。

https://stackoverflow.com/questions/62871727/using-class-instances-as-keys-in-a-typescript-map

> The JavaScript specification says Map uses the SameValueZero operation to determine whether keys are equal to each other

とあるように、Map は SameValueZero という操作で、キーが互いに等しいかどうかを判断します。

SameValueZero とは何かわからなかったので、そのままstack overflowに貼ってあったリンクを踏むとECMAScriptの仕様書にたどり着きました。

https://tc39.es/ecma262/#sec-samevaluezero

上記を読むと、

1. Typeが違うとfalseを返す
2. Typeがnumberの場合は [Number:sameValueZero](https://tc39.es/ecma262/#sec-samevaluenonnumber) の比較結果を返す
3. Typeがnumber以外の場合は [SameValueNonNumber](https://tc39.es/ecma262/#sec-samevaluenonnumber)の比較結果を返す

です。であれば、別々のインスタンスを渡す今のロジックでは、当然のようにvalueを取得することはできませんね。

実際に [Map.prototype.get](https://tc39.es/ecma262/#sec-map.prototype.get) にもSameValueZeroを利用すると書いてありました。ちゃんと読めばすぐにわかりましたね。

# 代替手段を考える
このままだと『テスト駆動開発』の多国通貨開発が進みません。今回の場合は、通貨の単位をPairクラスのインスタンス生成時に受け取るので、その文字列をもとにkeyとなり得る値を生成することで、実装していきたいと思います。

```typescript
class Bank {
  private rates: Map<string, number> = new Map();
  constructor() {}

  public addRate = (from: string, to: string, rate: number): void => {
    this.rates.set(new Pair(from, to).generateKey(), rate);
  };

  public rate = (from: string, to: string): number => {
    if (from === to) return 1;
    return this.rates.get(new Pair(from, to).generateKey()) ?? 1;
  };
}

class Pair {
  constructor(private from: string, private to: string) {
    this.from = from;
    this.to = to;
  }

  public generateKey = () => {
    return `${this.from}-${this.to}`;
  };
}

const bank: Bank = new Bank();
bank.addRate('CHF', 'USD', 2);
console.log(bank.rates('CHF', USD))
```

Mapのkeyとなり得る型をstringにして、無事テストを通すことができました（equalsメソッドを定義しても意味ないことがわかったので消しちゃいました）