はじめに
TypeScriptでよく行われる型変換をサポートするために、TypwScriptではユーティリティ型というものを提供しています。この記事ではそんなユーティリティ型を再実装してみようと思います。
全部で21ケースありますが、1ケース(ThisType
)は単なる空のマーカーインターフェイスで、4ケースはintrinsic型(TypeScriptのコンパイラに隠された型)なので残りの16ケースを紹介します。数が多いのでこの記事では6ケース紹介して、残りの10ケースは別記事に分けて紹介します。Part2はこちらです。Part3はこちらです。
Awaited
Awaited
はaync関数のawaitや、Promiseにおけるthenのような操作の結果を再起的に取得するような型です。
type Example01 = Awaited<Promise<string>>
とすればstring
を取得することもできますし、
type Example02 = Awaited<Promise<string | Promise<number>>
とするとstring | number
を得ることが出来ます。
このような関数を作るにはPromiseに渡される引数をPromiseが完全に剥がれるまで取得すれば良いので以下のように書くことで再現することが出来ます。
type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T;
T extends Promise<infer U>
でTがPromiseだった時にはその中身U
を取得するようにしています。そしてMyAwaited<U>
は取得したU
を再度自作したMyAwaited
に代入しています。これをPromiseではなくなるまで繰り返して、Promiseでは無くなったときにT
そのものを返しています。
再実装を紹介したところでTypeScriptの実装を見てみます。
type Awaited<T> =
T extends null | undefined ? T :
T extends object & { then(onfulfilled: infer F, ...args: infer _): any } ?
F extends ((value: infer V, ...args: infer _) => any) ?
Awaited<V> :
never :
T;
再実装したものと比べて難解な表現ですが、基本的に行なっていることは同じです。TypeScriptの実装ではPromiseから中身を取得するのではなく、Promiseが持つthenメソッドから中身を取得するようになっています。これによってPromiseの他のPromiseLikeなどのThenableな型に対応することができるわけです(つまり再実装の例ではPromiseLikeなどは扱えません)。
型の説明です。まずT
がnull
とundefined
である場合はすぐにその値を返します。そしてT
がThenableな型だった場合は引数onfulfilled
をV
として取得します。取得できなかった場合は非同期の型ではないと判断してT
を返します。V
はonfulfilled
関数の型で以下のようになっています。
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null
undefined
やnull
の場合を除けば関数の形になっているとしてF extends ((value: infer V, ...args: infer _) => any)
でV
(Promiseの中身)を抜き出します。そしてさらにV
をAwaited
に渡してThenableではなくなるまで処理を続けます。
Partial
Partial
は渡されたオブジェクトをオプショナルな型にします。
例えば
type User = {
id: number;
name: string;
mailaddress: string;
};
type PartialUser = Partial<User>;
のように使うことが出来ます。結果としては以下のようなものが得られます。
{
id?: number | undefined;
name?: string | undefined;
mailaddress?: string | undefined;
};
オプショナル化は再起的には行われず、浅い階層に対してのみ行われます。つまり下のような型は
type Admin = {
id: string;
type: string;
user: {
id: number;
name: string;
mailaddress: string;
};
};
type PartialAdmin = Partial<Admin>;
以下のようになります。
{
id?: string | undefined;
type?: string | undefined;
user?: {
id: number;
name: string;
mailaddress: string;
} | undefined;
}
このようなオブジェクトに作用させる処理はMapped Typesで大体は解決できます。この機能を利用すれば以下のように実装できます。
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
オブジェクトを組み直す際に?
をつけて一つずつオプショナル化するだけです。この実装はTypeScriptの実装とも一致します。
Required
Required
は先ほどのPartial
がオプショナル化するのに対して必須にする型です。
type PartialUser = {
id?: number | undefined;
name?: string | undefined;
mailaddress?: string | undefined;
};
type User = Required<PartialUser>;
上記の例はお察しの通り以下のようになります。
{
id: number;
name: string;
mailaddress: string;
};
浅い階層にしか処理が走らない振る舞いも同じです。
実装もPartial
とほとんど同じです。
type MyRequired<T> = {
[P in keyof T]-?: T[P];
};
-?
とすることでオプショナルが解けるというわけですね。TypeScriptの実装も同じようになっています。
Readonly
Readonly
もPartial
やRequired
と似た型です。オブジェクトに対して作用させる型で、そのオブジェクトを浅い階層でreadony
にします。Partial
やRequired
と異なる点として、配列に対しても有効な型となっています(厳密にはPartial
もRequired
も配列に対して作用しますが、利用する時がほとんどないので考えないものとしています)。
type Sample01 = Readonly<[number]>;
上記の例ではreadonly [number]
が結果とされます。
実装方法はPartial
などに似ていて以下のように行えます。
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};
配列の実装はキーがnumber
のオブジェクトのように扱えるので、実装時には配列の考慮を行わなくても良いです。
これもまたTypeScriptも同じ実装になっています。
Record
Record
はRecord<Keys, Type>
のような引数ととり、Keys
に渡したユニオンのそれぞれをキーとしてそれらがType
をバリューとするプロパティを持つオブジェクトを作成する型です。
type User = {
id: string;
name: string;
mailaddress: string;
};
を作りたい時は以下のように利用します。
type User = Record<'id' | 'name' | 'mailaddress', string>;
実装はこれまでになく簡単に行えます。Keys
を元にキーを組み立て、Type
をバリューにするだけだからです。
type MyRecord<Keys extends keyof never, Type> = {
[P in Keys]: Type;
};
TypeScriptとの違いは引数の命名の違いと、keyof never
がkeyof any
になっている箇所くらいです。keyof never
もkeyof any
もどちらもstring | number | symbol
なので意味していることは同じです。
Pick
Pick
はPick<Type, Keys>
のように使います。Keys
はType
のキーの一部で、Pick
はType
からKeys
に指定されたキーのプロパティのみを抜き出す型です。
type User = {
id: string;
name: string;
mailaddress: string;
};
type PickedUser = Pick<User, 'id' | 'name'>;
のようにするとPickedUser
は以下のような値になります。
{
id: string;
name: string;
}
実装は以下のようにします。
type MyPick<Type, Keys extends keyof Type> = {
[P in Keys]: Type[P];
};
Type
からKey
だけを取得するように展開しました。これも型名は異なりますが、TypeScriptの実装と同じです。