KAKAMU
BlogTag

Union型とIntersection型をkeyofと組み合わせる

そもそも

Union型とIntersection型をkeyofと組み合わせる場合の書き方を整理してみた。

Mapped Typesを作るときなど、複数の型からkeyを抽出するのに役立つ。

主な依存パッケージ

"devDependencies": {
    "typescript": "5.6.3"
}

似て非なる型たち

type KeyOfUnion<T, U> = keyof (T | U)
 
type UnionOfKeyOf<T, U> = keyof T | keyof U
 
type KeyOfIntersection<T, U> = keyof (T & U)
 
type IntersectionOfKeyOf<T, U> = keyof T & keyof U

具体例で検証してみる

type First = {
  id: string
  onlyInFirst: number
}
 
type Second = {
  id: string
  onlyInSecond: number
}
type KeyOfUnion<T, U> = keyof (T | U)
type KOU = KeyOfUnion<First, Second>
// type KOU = "id"
 
type UnionOfKeyOf<T, U> = keyof T | keyof U
type UOKO = UnionOfKeyOf<First, Second>
// type UOKO = "id" | "onlyInFirst" | "onlyInSecond"
 
type KeyOfIntersection<T, U> = keyof (T & U)
type KOI = KeyOfIntersection<First, Second>
// type KOI = "id" | "onlyInFirst" | "onlyInSecond"
 
type IntersectionOfKeyOf<T, U> = keyof T & keyof U
type IOKO = IntersectionOfKeyOf<First, Second>
// type IOKO = "id"

直感に反するところ

TypeScriptのUnion型(T | U)とIntersection型(T & U)を、値に対する条件文と同様に捉えていると、上記の検証結果は直感に反する。

keyof (T | U)は和集合っぽく見えるが抽出されるkeyは積集合になっており、逆にkeyof (T & U)は積集合っぽく見えるが抽出されるkeyは和集合になっているからだ。

型を「値の集合」として捉える

これらのややこしさを解消するには、型を「値の集合」として捉えるメンタルモデルに切り替える必要がある。

特にTypeScriptにおいては、型を集合論的に扱えるようなデザインが意図的になされており、型を「値の集合」として捉えることで直感的に型を理解することができるようになっています。

サバイバルTypeScript#集合論的なデザイン

keyof (T | U)の解説

T | Uは、Tに属する値の集合と、Uに属する値の集合の和集合を表現している。

T | Uは和集合だが、keyofT | Uに属するすべての値に存在することが保証されたプロパティにしかアクセスできないので、結果的にkeyが積集合のようになる。

このややこしさについては、公式ドキュメントでも言及されている。

It might be confusing that a union of types appears to have the intersection of those types’ properties. This is not an accident - the name union comes from type theory. The union number | string is composed by taking the union of the values from each type. Notice that given two sets with corresponding facts about each set, only the intersection of those facts applies to the union of the sets themselves. For example, if we had a room of tall people wearing hats, and another room of Spanish speakers wearing hats, after combining those rooms, the only thing we know about every person is that they must be wearing a hat.

Everyday Types#working-with-union-types

keyof (T & U)の解説

T & Uは、Tに属する値の集合と、Uに属する値の集合の積集合を表現している。

直感的に想起されるようなTUに共通するプロパティを抽出した型は表現していない。

T & Uは、TのすべてのプロパティとUのすべてのプロパティをもつので、結果的にkeyが和集合のようになる。

使用例

これまでの話を理解すると、次のような汎用的な型を理解できるようになる。

Diff

type Diff<T, U> = Omit<T & U, keyof (T | U)>

Common

type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
    ? true
    : false
 
type Common<
  T extends Record<string, unknown>,
  U extends Record<string, unknown>,
> = Pick<
  T,
  {
    [K in keyof T & keyof U]: Equal<T[K], U[K]> extends true ? K : never
  }[keyof T & keyof U]
>

参考書籍

プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで

参考リンク

サバイバルTypeScript#集合論的なデザイン

Everyday Types#working-with-union-types

TypeScript における型の集合性と階層性

#IT#TypeScript