LoginSignup
1
0

More than 1 year has passed since last update.

TypeScriptのユーティリティ型を再実装する Part2

Last updated at Posted at 2022-12-20

はじめに

TypeScriptではよく行われる型変換をサポートするために、ユーティリティ型というものを提供しています。この記事ではそんなユーティリティ型を再実装してみようと思います。全部で21ケースありますが、1ケース(ThisType)は単なる空のマーカインターフェイスで、4ケースはintrinsic型(TypeScriptのコンパイラに隠された型)なので残りの16ケースを紹介します。この記事では5ケース紹介します。Part1はこちらです。Part3はこちらです。

Omit

OmitOmit<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で組み直すときに、PKeysの一部であれば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

ExtractExcludeの逆で第二引数の型に含まれるものだけ残します。つまり

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 & {};

これでnullundefinedを弾けるのは直感的ではないですね。Intersection TypesA & BはAとBの両方の型が合わさったような型を生成します。つまりNonNullableは空のオブジェクト{}と引数を合成したような型を生成します。オブジェクトで構成されている型はこれとIntersectionをとっても型に変化はありません(あらゆるオブジェクトは{}を含んでいると言えるので)。しかし、オブジェクトでない型は{}を含んでいないし、{}もその型を含んでいないので、オブジェクトではない型は{}とIntersectionをとってもそんな型は存在しないということでneverとなってしまいます。そしてオブジェクトではない型はnullundefinedしか存在しないので、この型はnullundefinedを弾くことが出来ます(例えば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だけ取得して返すようになっています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0