はじめに
TypeScriptではよく行われる型変換をサポートするために、ユーティリティ型というものを提供しています。この記事ではそんなユーティリティ型を再実装してみようと思います。全部で21ケースありますが、1ケース(ThisType)は単なる空のマーカインターフェイスで、4ケースはintrinsic型(TypeScriptのコンパイラに隠された型)なので残りの16ケースを紹介します。この記事では5ケース紹介します。Part1はこちらです。Part3はこちらです。
Omit
OmitはOmit<Type, Key>のように書きます。TypeからKeyをキーに含むプロパティを除いたものを返す型です。
type User = {
id: string;
name: string;
mailaddress: string;
};
type OmitUser = Omit<User, 'mailaddress'>;
上記のように使用することができ、OmitUserは以下のような型を持ちます。
{
id: string;
name: string;
}
実装はオブジェクトを扱う時のお決まりのMapped Typesを用いて行います。
type MyOmit<Type, Keys extends keyof Type> = {
[P in keyof Type as P extends Keys ? never : P]: T[Key]
};
TypeをMapped Typesで組み直すときに、PがKeysの一部であればneverを返してプロパティを作成しないようにしました。
TypeScriptの実装は以下のようになっています。
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Exclude関数はすぐ下で紹介しますが、第一引数から第二引数を省くような型となっています。つまり、オブジェクトのキーから不要なキーを削除したものをオブジェクトからPickするように作られています。再実装したものもTypeScriptの実装も第二引数に渡せる型以外は動作は同じです。前者はオブジェクトを構成するキーしか渡せませんでしたが、後者はstring | number | symbolならなんでもOKです。どうせ省くのでkeyof anyとした方が汎用性は高いと考えています。
Exclude
Excludeは先述の通り第一引数から第二引数を省くような型です。以下のようにExclude<UnionType, ExcludeMembers>のように書くことができます。
type Sample01 = Exclude<string | number | (() => void), Function>;
第一引数から第二引数を省くので、結果はstring | numberとなります(Fuctionは関数を包含する型です)。
この型の実装は以下のように書くことが出来ます。
type MyExclude<UnionType, ExcludeMembers> = UnionType extends ExcludeMembers ? never : UnionType;
[Conditional Types]はstring | number extends anyのように左にユニオンを渡すとstring extends any | number extends anyのように分離する仕組みがあります。これはその仕組みを生かしたものとなっています。UnionTypeを分離してExcludeMembersに含まれるものだけneverにして削除しています(neverはユニオンとして扱うとEmptyになって消えます)。
TypeScriptの実装も型名以外は同じです。
Extract
ExtractはExcludeの逆で第二引数の型に含まれるものだけ残します。つまり
type Sample01 = Extract<string | number | (() => void), Function | symbol>;
とすると() => voidが得られます。一致するではなく、含まれるというのがポイントです(上記の例ではsymbolではないが、() => voidが残った)。
実装もExcludeの条件分岐を逆にするだけです。
type MyExtract<Type, Union> = Type extends Union ? Type : never;
これも、TypeScriptの実装は型名以外は同じです。
NonNullable
NonNullableはnullとundefinedを引数から省く型です。
type Sample01 = NonNullable<string | number| null | undefined>;
だとstring | numberとなります。
実装は先ほどのExcludeを使って
type MyNonNullable01<Type> = Exclude<Type, null | undefined>;
またはExcludeを使わずに
type MyNonNullable02<Type> = Type extends null | undefined ? never : Type;
のように行いました。直感的ですね。
TypeScriptでは以下のようになっています。
type NonNullable<T> = T & {};
これでnullとundefinedを弾けるのは直感的ではないですね。Intersection TypesA & BはAとBの両方の型が合わさったような型を生成します。つまりNonNullableは空のオブジェクト{}と引数を合成したような型を生成します。オブジェクトで構成されている型はこれとIntersectionをとっても型に変化はありません(あらゆるオブジェクトは{}を含んでいると言えるので)。しかし、オブジェクトでない型は{}を含んでいないし、{}もその型を含んでいないので、オブジェクトではない型は{}とIntersectionをとってもそんな型は存在しないということでneverとなってしまいます。そしてオブジェクトではない型はnullとundefinedしか存在しないので、この型はnullとundefinedを弾くことが出来ます(例えばstringはプリミティブでオブジェクトではなさそうですが、sliceなどのプロパティを持つオブジェクトでもあるので弾かれません)。
Parameters
Parametersは関数のパラメータを取得する関数です。
type SampleFunc = (arg1: string, arg2: number) => void;
type SampleParameters = Parameters<SampleFunc>;
のように書くことができて[arg1: string, arg2: number]のような結果が返ってきます。Parameters<Function>と出来ないことに注意してください。
interface Function {
apply(this: Function, thisArg: any, argArray?: any): any;
call(this: Function, thisArg: any, ...argArray: any[]): any;
bind(this: Function, thisArg: any, ...argArray: any[]): any;
toString(): string;
prototype: any;
readonly length: number;
arguments: any;
caller: Function;
}
この構造が欲しいわけではなく、(...args: any): anyが欲しいというわけです。
実装は以下のようになります。
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
TypeScriptの実装そのままですが、(...args: any) => any>からT extends (...args: infer P) => anyで必要なPだけ取得して返すようになっています。