23
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-06-08

はじめに

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

この記事では

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

たとえば、次のような例です。

type User = {
  id: number;
  name: string;
}
type ReadonlyUser = {
  readonly id: number;
  readonly name: string;
}

ReadonlyUserUser と同じ構造に見えますが、keyof User を使っておらず、手動で定義された型なので、TSからは「構造の派生ではない(= 別物)」と見なされます。

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

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

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

  • 型の構造を保ちながら変形できる
    元の型の構造を活かしたまま、プロパティの修飾(optionalreadonly など)を加えることができます。

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

// 各キーをoptional + nullに変換するユーティリティ型
type NullablePartial<T> = {
  [K in keyof T]?: T[K] | null
}
  • 補完や型推論の精度が高い
    構造が保たれているので、IDE(例:VSCode)でも型補完が正確になります。

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

実際の実装例

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

たとえば、次の User 型があるとします。

type User = {
  id: number;
  name: string;
  email: string;
}

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

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

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

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

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; }' では必須です。

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

対して、静的な定義では。。。

type UserRequireId = {
  readonly id: number;
  readonly name: string;
  readonly email: string;
}

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

&で型を合成するのと何が違うの?

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

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

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

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

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

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

具体例で比較してみよう!

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

&による型合成(静的に定義した型)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

おわりに

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

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

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

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

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

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

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


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

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

ではでは!

参考

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

23
8
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?