はじめに
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
だけ取得して返すようになっています。