【TypeScript】Map<K, V>とRecord<K, V>ってなにが違うの？

# はじめに

こんにちは ken です。最近は TypeScript を勉強しています。
TypeScript には `Map` 型と `Record` 型がありますよね。
私はこれまで Go と Python しか触れてこなかったため、key と value の組のセットといえば `Map` というイメージでした。
ただ今回新たな概念として `Record` 型というものが登場したので自身の勉強も兼ねて記事にまとめてみました。
正確性には気を付けて書いていますが、初学者による記事のため誤り等あるかもしれません。
その場合はコメントでご指摘ください :pray:

# Map vs Record

## Map 型とは

`Map` 型は、JavaScript の `Map` オブジェクトを型としたもののようです。

MDN による[Map](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map)の説明を引用します。

> Map オブジェクトはキーと値のペアを保持し、キーが最初に挿入された順序を覚えています。 キーや値には任意の値（オブジェクトとプリミティブ値）を使用することができます。

なるほど。今まで慣れ親しんできた "Map" という感じがします。
MDN の説明を読むと `Map` には以下の特徴があるとわかります。

- キーとして任意の型（関数、オブジェクト、あらゆるプリミティブ型）を使用可能
- プロトタイプメソッド（例: `set`, `get`, `has` など）を持つ
- イテラブル（`for...of`など）

## Record 型とは

`Record` 型は TypeScript 独自のユーティリティ型のようです。

Microsoft による TypeScript の公式ドキュメントには次の記載があります。
https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type

> Constructs an object type whose property keys are Keys and whose property values are Type. This utility can be used to map the properties of a type to another type.
> （筆者訳：キーが Keys で、バリューが Type であるオブジェクト型を構築します。このユーティリティは、ある型のプロパティを別の型にマッピングするために使用できます。）

こちらも Key と Value の組ではあるものの、その正体はオブジェクト型のようです。

[実装](https://github.com/microsoft/TypeScript/blob/5d8ef4bdcb8bdbd4307c74a07b01e3bdf6e04b6a/lib/lib.es5.d.ts#L1590-L1592)を見てみると次のように定義されていました。

```ts
type Record<K extends keyof any, T> = {
  [P in K]: T;
};
```

`Mapped Types` を使った素直な実装ですね。

`Record` 型がオブジェクトであるということから以下のような特徴があるとわかります。
- キーとして使用できる型は `string`、`number`、`symbol` のみ
- 通常のオブジェクトとして扱われるため、オブジェクトのメソッドが使用可能

## 両者の特徴比較

ここまででわかった `Map` 型と `Record` 型の違いを表でまとめてみました。

| 特徴           | Map                                          | Record                            |
| -------------- | -------------------------------------------- | --------------------------------- |
| キーの型       | 任意（オブジェクト含む）                     | `string`,`number`,  `symbol` のみ |
| メソッド       | Map 固有のメソッド（`set`, `get`, `has` 等） | 通常のオブジェクトメソッド        |
| イテレーション | `for...of`, `forEach` 等が使用可能           | `Object.entries` 等が必要         |
| 構造の柔軟性   | 動的（キーや値を自由に追加・削除可能）       | 静的（固定されたキー構造）        |

## 使い分け

`Map`と`Record`の違いについてはこれでわかりましたが、KeyとValueの組を保持したい場合はどちらを使えばよいのかを考えてみました。
### Map が適している場合：


**キーとしてオブジェクトを使用する必要がある場合**
   `Record` 型はオブジェクトを Key にはとれないので、オブジェクトを Key に使いたい場合は `Map` を使わざるを得ません。

```typescript
// Map型の場合：✅ objectをKeyにとれる
const objectRelations = new Map<object, string>();
const obj1 = { id: 1 };
objectRelations.set(obj1, "hoge");

// Record型の場合：❌ objectをKeyにとれない
type objectRelationsByRecord = Record<object, string>; // Type 'object' does not satisfy the constraint 'string | number | symbol'
```

### Record が適している場合：

**列挙型やユニオン型とのマッピングが必要な場合**
   特定の文字列リテラル型やユニオン型をキーとして、それに対応する値の型を定義する場合に便利です。これにより、すべてのケースに対する値の定義が強制されます。一方、`Map` 型の場合は実行時まで各キーに対する値の設定が任意となり、必要なキーの実装漏れを防ぐことができません。

```typescript
type Status = "pending" | "success" | "error";
type StatusMessage = Record<Status, string>;

// Record型の場合：コンパイル時に全てのキーの実装が強制される
const messages: StatusMessage = {
  pending: "処理中です",
  success: "成功しました",
  error: "エラーが発生しました",
}; // ✅ OK

// Map型の場合：コンパイル時のチェックが緩い
const messagesMap = new Map<Status, string>();
messagesMap.set("pending", "処理中です");
messagesMap.set("success", "成功しました");
// error のケースが抜けていてもコンパイルエラーにならない ⚠️
```

# 最後に

今回は `Map` 型と `Record` 型について勉強を兼ねてまとめてみました。
確かに似てはいましたが、それぞれに特性があり全くの別物ということがわかってよかったです。
間違いなどありましたらコメントにてご指摘ください、ここまで読んでいただきありがとうございました。

## 参考

https://typescriptbook.jp/reference/builtin-api/map

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Map

https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type