【TypeScript】「ホモモーフィック」なマップ型とは何か？

# はじめに

TypeScript（以下TS）の型の中で、あまり知られていない「**ホモモーフィック（homomorphic）**」なマップ型という概念があります。

この記事では

* ホモモーフィックなマップ型とは?
* どんな型がホモモーフィックで、どんな型が非ホモモーフィックなのか？
* メリットや使いどころ
* 実際の使用例

について、できるだけ丁寧に・分かりやすくご紹介していますので良かったら見てみてください！！










## ホモモーフィックなマップ型ってなに？


TSでは、あるオブジェクト型 `T` に対して、そのキー（`keyof T`）を使って動的に新しい型を作ることができます。


たとえば、次のような型定義です。
```ts
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K]
}
```

これは「元の型の構造をベースに、すべてのプロパティに `readonly` を付けた型」を定義しています。

このように、元の構造に忠実に変形された型のことを、TSでは「ホモモーフィック・マップ型（homomorphic mapped type）」と呼びます。

つまり、`keyof T` を使って元の型をなぞるように処理する型ですね。

`keyof T` を使って `T` のキーをループしてるので、 `T` との関連をちゃんと認識してくれる「ホモモーフィック」な型なのです。


::: note
`keyof T`に限らず、元の構造を意識して動的に定義された型はホモモーフィックと見なされます。
:::










## 非ホモモーフィックなマップ型とは？

一方で、元の型 `T` のキーを使わずに、まったく別の型を定義すると、それは「非ホモモーフィック」な型になります。

TSは「これは `T` を変形したものじゃなくて、ただ別物を作ったんだね」とみなして、特別な型処理（例：ユーティリティ型との連携など）をしません。

以下が非ホモモーフィックなマップ型の例になります。

たとえば、次のような例です。
```ts
type User = {
  id: number;
  name: string;
}
type ReadonlyUser = {
  readonly id: number;
  readonly name: string;
}
```
`ReadonlyUser` は `User` と同じ構造に見えますが、`keyof User` を使っておらず、手動で定義された型なので、TSからは「構造の派生ではない（= 別物）」と見なされます。

つまり、元の型に依存せず完全に新しく定義された型は、TSから見ると「別物」と扱われるということですね。



## ホモモーフィックの何がメリットなの？

ホモモーフィックなマップ型には、次のようなメリットがあります。

- **型の構造を保ちながら変形できる**
元の型の構造を活かしたまま、プロパティの修飾（`optional`、`readonly` など）を加えることができます。


- **ユーティリティ型の自作がしやすい**
`Partial<T>` や `Readonly<T>` のような型を、簡潔に定義できます。

```ts
// 各キーをoptional + nullに変換するユーティリティ型
type NullablePartial<T> = {
  [K in keyof T]?: T[K] | null
}
```

- **補完や型推論の精度が高い**
構造が保たれているので、IDE（例：VSCode）でも型補完が正確になります。

- **変更に強く、保守性が高い**
元の型 `T` に変更があっても型エラーで気づきやすく、再利用性もバッチリ！










## 実際の実装例

実際の実装例を見てみましょう！

たとえば、次の User 型があるとします。
```ts
type User = {
  id: number;
  name: string;
  email: string;
}
```

このとき、`id` は必須だけど、その他の項目（name, email）は省略可能にしたいケースってよくありますよね。


そんなときに役立つのが、以下のような型変換です。

```ts
type RequireId<T extends { id: unknown }> = {
  id: T['id']
} & {
  [K in Exclude<keyof T, 'id'>]?: T[K]
}
```

この型では `id` プロパティだけは必須に固定しつつ、それ以外のはオプショナルに変換するホモモーフィックな型定義です。

```ts
const user1: RequireId<User> = {
  id: 1
} // OK！ nameやemailは省略可能

const user2: RequireId<User> = {
  id: 2,
  name: '田中'
} // OK！ emailは省略可能

const user3: RequireId<User> = {
  name: '田中',
  email: 'hogehoge@example.com'
} // NG！ idは省略不可
// 型 '{ name: string; email: string; }' を型 'RequireId<User>' に割り当てることはできません。
// プロパティ 'id' は型 '{ name: string; email: string; }' にありませんが、型 '{ id: number; }' では必須です。
```

このように、「一部だけ特別扱い＋その他は動的に処理」も、ホモモーフィックな型なら柔軟に実現できます。

対して、静的な定義では。。。
```ts
type UserRequireId = {
  readonly id: number;
  readonly name: string;
  readonly email: string;
}
```

より複雑なオブジェクトの場合だと冗長になりがちですし、やはりマップ型を使えば簡潔＆再利用性もバッチリですね！













## `&`で型を合成するのと何が違うの？


型の拡張によく使われる `&`（交差型）とホモモーフィックな型の違いも明確にしておきましょう。

例えば、次のような型定義です。

```ts
type WithTimestamp<T> = T & { createdAt: Date }
```

一見すると「元の型を拡張してる」ように見えますが、これはホモモーフィックな型ではありません。
なぜなら、これは `T` の中身に応じて動的に変形しているわけではなく、単純に固定のプロパティ（createdAt）を追加しているだけだからです。

一方で、ホモモーフィックなマップ型は `keyof T` を使っている場合、`T` のキーそれぞれに対して何かしらの処理を施すというのがポイントです。


つまり、構造を「なぞる」ような変換ができるかどうかが、ホモモーフィックかどうかの分かれ道なんです！








## 具体例で比較してみよう！

ここでは「すべてのプロパティを `readonly` にする」という同じ目的を、「静的に定義した型」と「ホモモーフィックなマップ型」で比較します。

### `&`による型合成（静的に定義した型）

```ts
type ReadonlyUser = {
  readonly id: number
  readonly name: string
}
```

この型は、元の型 `User` のキーを手動で書き直して `readonly` を付けたものです。
`keyof T` を使っていないので、TSから見ると「**新しく作られた別の型**」であり、ホモモーフィックとは見なされません。

#### ホモモーフィックな型変換

```ts
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K]
}
// const hoge: MyReadonly<User> = ..
```

こちらは `keyof T` を使って元の構造をループし、すべてのプロパティに `readonly` を動的に適用しています。
`T` の構造をベースにした型変換であるため、TSはこれをホモモーフィックと判断します。

→ **`&` は「足し算」的な操作、ホモモーフィックは「構造変形」を伴う高度な操作が可能なのです！**















## いつホモモーフィック型を使うべき？それとも使わなくてもいい？

ここまでで、「ホモモーフィックな型は便利！」という話をしてきましたが、
だからといって、**どんな場面でも使うべき**というわけではありません。

以下に、使うべき場面と、あえて使わないほうがよいケースを整理して紹介します。

### ✅ ホモモーフィックな型を使うべきケース

#### ■ 既存の型 `T` に沿った変形をしたいとき

型を一括で変換したい場面では、マップ型で `keyof T` を使うのが非常に効果的です。
各プロパティの修飾子（readonlyなど）も保ったまま柔軟に変形できます。

```ts
type MyOptional<T> = {
  [K in keyof T]?: T[K]
}
```

#### ■ `Partial<T>`, `Pick<T, K>`, `Readonly<T>` などのように、構造を維持しながら型を柔軟に変えたいとき

元の型構造をそのまま活かして、「部分的な適用」や「限定的な利用」ができるようにしたい場面などに有効です。

```ts
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K]
}
```

#### ■ 一部プロパティを特別扱いしつつ、他を動的に処理したいとき

全体を変形するのではなく、一部はそのまま、他は動的に加工したいときに効果を発揮します。

```ts
type ReadonlyExceptId<T> = {
  id: T['id']
} & {
  [K in Exclude<keyof T, 'id'>]: Readonly<T[K]>
}
```

### ❌ ホモモーフィックを使わなくてもいいケース

#### ■ Tの構造に依存せず、静的にプロパティを追加するだけのとき

型の構造を変形する必要がなく、ただ項目を追加するだけなら、ホモモーフィックである必要はありません。

```ts
type WithTimestamp<T> = T & { createdAt: Date }
```

#### ■ ユニオン型やパターンマッチ型など、構造の一貫性が求められない場合

明確に分岐する構造（success/errorなど）では、マップ型の柔軟さよりも可読性重視で設計すべきです。
そのため、ホモモーフィックである必要はありません。

```ts
type Result =
  | { status: 'success'; data: string }
  | { status: 'error'; error: Error }
```

#### ■ 再利用性が重視されない一発定義などのとき

再利用性や保守性よりも、単純明快で軽量な型のほうが優先される場合は、静的な定義で十分です。

```ts
type Simple = { name: string } & { age: number }
```

---

このように、ホモモーフィックなマップ型はとても便利な一面もありますが、目的や状況に応じて**適材適所で使い分けること**が大事です。
過度に抽象的な型変換を行うと、逆に型定義が読みにくくなることもあるため、可読性と保守性のバランスを考慮するのがベストです。












## おわりに

ここまで読んでくださって、本当にありがとうございました！

TSで型を扱っていると、「**新しく型を定義するべきか？**」「**既存の型をどう変形するか？**」といった悩みにぶつかること、結構ありますよね。

私自身、日々TSを書いていても、

- 新しい型を一から定義するべき？
- `extends` で継承したほうがいい？
- それともマップ型で動的に変形すべき？

…などなど、毎回のように迷ってしまうことがあります。

今回「ホモモーフィック型」という切り口で整理することで、
「**構造をなぞるべきか？**」「**個別に定義するべきか？**」の判断軸が少し見えてきたような気がします。

やっぱり、型って奥が深いですね……(*´-`)

---

あらためて、最後まで読んでくださりありがとうございます。

もし記事が参考になったら、「**いいね**」と「**ストック**」をしてもらえるとすごく励みになります！
また、内容に誤りや気になる点があれば、遠慮なくご指摘していただけると嬉しいです！
他にもいろいろな記事を投稿しているので、もしよかったら見てみてください！

ではでは！

https://qiita.com/rtkjm22/items/cd45058a2631cd7086fd

https://qiita.com/rtkjm22/items/84d253631f518f8e6fff

https://qiita.com/rtkjm22/items/ee08228f170c9e6e81a3

https://qiita.com/rtkjm22/items/16bf595fb8b8a7b50aa5

https://qiita.com/rtkjm22/items/9ac98dbb4e3f5a37c43c



## 参考
記事を執筆するにあたって、以下の資料を参考にさせていただきました。
先人たちの知見に感謝です！

https://www.oreilly.co.jp/books/9784814401093/

https://zenn.dev/uhyo/articles/array-homomorphic-mapped-type

https://qiita.com/axoloto210/items/e26ed657f8021b813eee

https://stackoverflow.com/questions/59790508/what-does-homomorphic-mapped-type-mean

https://qiita.com/axoloto210/items/9020310b88f68190b6e7