2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

1人フロントエンドAdvent Calendar 2022

Day 20

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

Last updated at Posted at 2022-12-19

はじめに

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などは扱えません)。
型の説明です。まずTnullundefinedである場合はすぐにその値を返します。そしてTがThenableな型だった場合は引数onfulfilledVとして取得します。取得できなかった場合は非同期の型ではないと判断してTを返します。Vonfulfilled関数の型で以下のようになっています。

onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null

undefinednullの場合を除けば関数の形になっているとしてF extends ((value: infer V, ...args: infer _) => any)V(Promiseの中身)を抜き出します。そしてさらにVAwaitedに渡して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

ReadonlyPartialRequiredと似た型です。オブジェクトに対して作用させる型で、そのオブジェクトを浅い階層でreadonyにします。PartialRequiredと異なる点として、配列に対しても有効な型となっています(厳密にはPartialRequiredも配列に対して作用しますが、利用する時がほとんどないので考えないものとしています)。

type Sample01 = Readonly<[number]>;

上記の例ではreadonly [number]が結果とされます。
実装方法はPartialなどに似ていて以下のように行えます。

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

配列の実装はキーがnumberのオブジェクトのように扱えるので、実装時には配列の考慮を行わなくても良いです。
これもまたTypeScriptも同じ実装になっています。

Record

RecordRecord<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 neverkeyof anyになっている箇所くらいです。keyof neverkeyof anyもどちらもstring | number | symbolなので意味していることは同じです。

Pick

PickPick<Type, Keys>のように使います。KeysTypeのキーの一部で、PickTypeから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の実装と同じです。

2
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?