🎄

Mapped Typesを集合と写像の観点から捉える

2023/12/22に公開

Mapped Types

Mapped Typesは他の型をもとにオブジェクト型を生成できる機能で、{[P in T]: U}という構文で表されます。

type Obj = {
    num: number
    str: string
    foo: boolean
}

type MappedObj = {
    [PropertyKey in keyof Obj]: string | number
}
// type MappedObj = {
//     num: string | number;
//     str: string | number;
//     foo: string | number;
// }

上のコード例では、Obj型のプロパティキーをもち、値の型がstring | numberであるようなオブジェクト型MappedObjが新たに生成されています。
 
{[P in T]: U}という構文のTに使用できる型はプロパティのキーとして指定できる型[1]
の部分型である必要があります。

type MappedObj = {
    [PropertyKey in Boolean]: string | number
}
Type 'Boolean' is not assignable to type 'string | number | symbol'.(2322)

集合・写像との関連

Mapped Typesという名前にあるmap は数学では写像を意味します。
写像の定義は次のようになっています。
ABを集合とするとき、Aのどんな元に対してもBの元を一つずつ対応させる規則が与えられた場合、その規則そのもののことを集合Aから集合Bへの関数または写像という.」[2]
 
この定義から、Mapped Types{[P in T]: U}は写像であると捉えることができます。
Tをプロパティキーの集合と捉えると、集合Tの元Pに対して、プロパティ値の型の集合の元Uを一つずつ対応させていますので、この規則は確かに写像であると捉えられると思います。

Mapping Modifiers (マッピング修飾子)

Mapped Typesにはマッピング修飾子というものを指定することで、プロパティをオプショナルなものにしたり、readonlyにすることができます。
readonlyとしたい場合には、{readonly [P in T]: U}のようにreadonlyを追加すればよく、オプショナルにしたい場合には、{[P in T]?: U}のように?を追加することで実現できます。
また、{-readonly [P in T]: U}{[P in T]-?: U}のように-を付加することでreadonlyを外したり、オプショナルでなくすこともできます。

type Obj = {
    num?: number
    readonly str: string
    foo: boolean
}

type MappedObj = {
    -readonly [PropertyKey in keyof Obj] ?: Obj[PropertyKey]
}
// type MappedObj = {
//     num?: number | undefined;
//     str?: string | undefined;
//     foo?: boolean | undefined;
// }

この-readonly?はマッピング修飾子(Mapped Modifiers)と呼ばれ、Readonly型やRequired型など、ユーティリティ型の実装にも使用されています。
https://zenn.dev/axoloto210/articles/advent-calender-2023-day21

as によるKey Remapping

Mapped TypesにはKey Remapping というプロパティキーに変更を加える機能が備わっています。
{[P in T as V]: U}という構文で表され、Vに使用できるのはTと同じくstring | number | symbolの部分型のみです。

type Obj = {
    num: number
    str: string
    foo: boolean
}


type MappedObj = {
    [PropertyKey in keyof Obj as `${PropertyKey}Optional`]?: Obj[PropertyKey]
}
// type MappedObj = {
//     numOptional?: number | undefined;
//     strOptional?: string | undefined;
//     fooOptional?: boolean | undefined;
// }

上のコード例では、keyof Obj型に含まれるプロパティキーnumstrfooのそれぞれに対して、プロパティキーをas `${PropertyKey}Optional`を通してnumOptionalstrOptionalfooOptionalに変換し(remapping)、値の型Obj[PropertyKey]をオプショナルにして対応づけています。

Key Remappingは合成写像?

集合keyof Objの元PropertyKeyObj[PropertyKey]をそれぞれ対応させる写像をf、集合keyof Objの元PropertyKey`${PropertyKey}Optional`へ対応させる写像をgとすると、MappedObj型はf∘gという合成写像によってf(g(keyof Obj))として生成されていると解釈できるのではないかと考えました。

しかし合成写像であるならば、最終的に得られる型は{`${PropertyKey}Optional`: Obj[`${PropertyKey}Optional`]}となってしまい、値の型が存在しないものとなってしまいます。
実際に得られる型{`${PropertyKey}Optional`: Obj[PropertyKey]}のプロパティは写像を使って表してみると、g(PropertyKey): Obj[PropertyKey]という対応になっています。
これは、PropertyKeyに写像fが適用された後に、プロパティキーに対してのみ写像gが適用された形となっています。

プロパティキーのみが写像gによって形を変えることから、Key Remappingという「キーのみが再度写像によってうつされる」ことを表した命名となっているのだろうと感じますね。

脚注
  1. string | number | symbol型のことで、keyofの上界とも呼ばれる型です。 ↩︎

  2. 引用:内田伏一.「集合と位相」(1986). 裳華房. p.16 ↩︎

GitHubで編集を提案

Discussion