火曜日, 6月 10, 2025
- Advertisment -
ホームニューステックニュース【TypeScript】「ホモモーフィック」なマップ型とは何か? #JavaScript - Qiita

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



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

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

この記事では

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

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

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

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

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

type MyReadonlyT> = {
  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 など)を加えることができます。

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

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

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

実際の実装例

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

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

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

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

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

type RequireIdT extends { id: unknown }> = {
  id: T['id']
} & {
  [K in Excludekeyof T, 'id'>]?: T[K]
}

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

const user1: RequireIdUser> = {
  id: 1
} // OK! nameやemailは省略可能

const user2: RequireIdUser> = {
  id: 2,
  name: '田中'
} // OK! emailは省略可能

const user3: RequireIdUser> = {
  name: '田中',
  email: '[email protected]'
} // NG! idは省略不可
// 型 '{ name: string; email: string; }' を型 'RequireId' に割り当てることはできません。
// プロパティ 'id' は型 '{ name: string; email: string; }' にありませんが、型 '{ id: number; }' では必須です。

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

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

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

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

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

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

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

type WithTimestampT> = T & { createdAt: Date }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

type ReadonlyExceptIdT> = {
  id: T['id']
} & {
  [K in Excludekeyof T, 'id'>]: ReadonlyT[K]>
}

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

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

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

type WithTimestampT> = T & { createdAt: Date }

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

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

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

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

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

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

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

おわりに

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

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

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

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

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

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

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


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

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

ではでは!

参考

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





Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -