🎄

PickやPartialなどのユーティリティ型はどのように作られているか?

2023/12/21に公開

ユーティリティ型(組み込み型)

TypeScriptにはよく使われる形の型や汎用性の高い便利な型がすでに用意されています(TypeScriptに標準で組み込まれている)。
ユーティリティ型は、keyof型、lookup型、Mapped Types、Conditional Types などの組み合わせによって実装されているケースが多いです。
https://zenn.dev/axoloto210/articles/advent-calender-2023-day17
よく使われている組み込み型がどのように実装されているかをみていきたいと思います。

Readonly

Readonly<T>型は、引数として受け取ったオブジェクト型Tのプロパティをreadonlyにします。

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

Readonly型は上のような実装となっており、Mapped Typesとlookup型の組み合わせによって実装されていることがわかります。
T[P]の部分がlookup型です。オブジェクト型Tに含まれるプロパティキーPをもつプロパティの型がT[P]です。
[P in keyof T]: T[P]の部分がMapped Typesと呼ばれるもので、オブジェクト型Tに含まれるプロパティの型がT[P]となります。つまり、オブジェクト型Tのコピーがこの部分で作られているわけですね。
Mapped Typesにはmapping修飾子の設定が可能で、readonlyとオプショナルプロパティ?を付加もしくは-readonly-?による除去が可能となっています。Tのコピーの型のプロパティキーにreadonlyをつけることで、各プロパティへのreadonly付与が実現できています。
Readonly<T>Tのプロパティに含まれるオブジェクトのプロパティまではreadonlyとしない性質が知られていますが、Tのプロパティキーそれぞれにのみreadonlyがつけられているためであることが実装からわかります。

Pick

Pick<T, K>型は、オブジェクト型TからKで指定したキーをもつプロパティを取り出して新しいオブジェクト型を作り出す組み込み型です。KにはT型のキーをユニオン型で指定できます。

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

Pick型の実装はこのようになっています。
K extends keyof T部分によって、KTのプロパティキーのユニオン型の部分型であるという条件が課されています。
Mapped Typesによって、Kを構成するキーをもつプロパティのみで構成されるオブジェクト型が得られています。オブジェクト型Tをコピーして新たに型をつくるという構造がReadonly<T>型と共通しており、実装が似た形となっていますね。

Partial

Partial<T>型は、型Tのプロパティをオプショナルにする型です。

type Partial<T> = {
    [P in keyof T]?: T[P];
};

Partial<T>型の実装はReadonly<T>とほとんど同じ実装になっています。
オブジェクトの型Tの各プロパティをMapped Typesとmapping修飾子?によってオプショナルなものにしています。

Required

Required<T>型は、Partial<T>型とは逆で、オプショナルな型をオプショナルではなくします。実装としては、mapping修飾子が-?となっている点のみがPartial型と異なります。

type Required<T> = {
    [P in keyof T]-?: T[P];
};

Extract

Extract<T, U>型は、型Tから型Uの部分型となる型を抽出する組み込み型です。Tにはユニオン型が指定されることが多いです。

type Extract<T, U> = T extends U ? T : never;

Extract<T, U>型はConditional Typesを使用して実装されています。
Conditional Typesは三項演算子と似た構文で、例えばT extends U ? T : neverの場合、TUの部分型、つまりT extends Uという条件を満たすならばT型となり、部分型でなければnever型となります。
このConditional TypesはTにユニオン型を指定した場合にはユニオン型の分配が行われるという少々難解な特徴があります。

type ToArray<T> = T extends string ? T[] : never

//type UnionArray = "octopus"[] | "squid"[]
type UnionArray = ToArray<"octopus"|"squid"> 

UnionArrayの型としては、"(octopus"|"squid")[]という型になりそうですが、実際にはユニオン型の構成要素が分配され、"octopus"[] | "squid"[]型となります。
Extract<T, U>型はユニオン型の分配という特徴を知らずとも、Conditional Typesによる型の抽出が行える点で意義がありそうですね。

Exclude

Exclude<T, U>型は、Extract<T, U>とは反対に、型Tから型Uの部分型となる型を取り除く組み込み型です。

type Exclude<T, U> = T extends U ? never : T;

元のコードもExclude<T, U>の場合と結果が逆になるよう実装されていますね。

Omit

Omit<T, K>型はPick<T, K>型とは反対に、Kで指定したキーをもつプロパティを除いたオブジェクト型を返します。

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Omit<T, K>の実装はPickExcludeを利用して実装されています。
Pick型とは異なり、Omit型のKに課されている制約はK extends keyof anyとなっており、Tのキー以外のプロパティキーも指定が可能な実装となっています。
Exclude<keyof T, K>の部分でTのプロパティキーからKで指定したキーを取り除いた型が得られます。
つまり、Pick<T, Exclude<keyof T, K>>Tのプロパティの内、Kに含まれるキーをもたないプロパティのみで新しくオブジェクト型を構成していることとなります。

そのほかのユーティリティ型

汎用性が高いが実装が難解な型付けをユーティリティ型が担ってくれています。
今回みた型の他にも数多のユーティリティ型がTypeScriptには用意されていますので、複雑な型を定義する前に使える型がないか探してみるのもよさそうですね。
https://www.typescriptlang.org/docs/handbook/utility-types.html

GitHubで編集を提案

Discussion