🤟

Type Challengesを始めよう!

2023/04/11に公開

こんにちは
フロントエンドエンジニアのぴです

最近会社の他の社員の方と、Type Challengesをやり直し始めたので、入門記事としてUtility Typesをいくつか作ってみます。

以前個人ブログに残した内容を、少し手直しした内容になっています。

ターゲット

  • TypeScript 普段使ってる人
  • Utility Types 使うけど、型作ったことない人

今回作る型

  • Partial
  • Pick
  • ReturnType

この三つを作りながら、必要な知識をまとめていきます。

Partial

Partialは、型のすべてのプロパティが Optional に設定された型です。

interface Person {
  surname: string;
  middleName?: string;
  givenName: string;
}

type T = Partial<Person>;
//   ^? {
//   surname?: string
//   middleName?: string
//   givenName?: string
// }

実装の進め方

Playground を開いて実装をします


開くとこんな画面になるので、type MyPartial = anyの部分を書き換えて実装します。

正しく実装できると、右側に出てるエラーが消えます。

実装に必要な知識

Partial を実装するのに必要な知識としては、大きく以下の 4 つです

それぞれ詳しくみていきます

keyof 型演算子

keyofはオブジェクト型からプロパティ名を型として返す型演算子です。

type Person = {
  name: string;
};
type PersonKey = keyof Person;
//   ^? "name"

オブジェクトのプロパティが複数ある場合は、全てのプロパティ名がユニオンとして返されます

type Book = {
  title: string;
  price: number;
  rating: number;
};
type BookKey = keyof Book;
//   ^? "title" | "price" | "rating"

keyof型演算子を使うと、オブジェクトのプロパティ名を取り出した型を定義したいときに、同じ型を定義しなくて済むので保守性が上がって便利になります。

インデックス型

オブジェクトのプロパティ名を指定せずに、プロパティの型のみを指定したい場合に使う型です。
プロパティの型が全てnumberなオブジェクトの型は以下のように定義できます。

let obj: {
  [K: string]: number;
};

Mapped Types

先ほどのインデックス型ではどんなプロパティ名でも型的にアクセスができるので、アクセスする時には毎回undefinedをチェックする必要があります。
形式が決まっている場合はMapped Typesを用いてこれを解決できます。

Mapped Typesはユニオン型をインデックス型のキーの制約として使うイメージ(表現あってる?)

type SystemSupportLanguage = "en" | "ja";

type Butterfly = {
  [key in SystemSupportLanguage]: string;
};
//   ^? {
  en: string
  ja: string
}

ユニオン型をキーの制約に使えるので、先ほどのkeyof演算子と組み合わせて使うことが多いです。

インデックスアクセス型

オブジェクトのプロパティの型や配列の型を参照する方法です。

type A = { foo: number };
type Foo = A["foo"];
//   ^? number

ユニオン型も使うことができます。

type Person = { name: string; age: number };
type T = Person["name" | "age"];
//   ^? string | number

keyof型演算子と組み合わせると、オブジェクトのプロパティの型を全てユニオンで取ることができます。

type Foo = { a: number; b: string; c: boolean };
type T = Foo[keyof Foo];
//   ^? string | number | boolean

だいぶ型の表現の幅が広がってきました!!
ここまでで察しが良い人なら気づいたかもしれませんが、Partialを作る準備ができました。

実装する

まずはMapped Typekeyof型演算子、インデックス型を組み合わせて、プロパティ名の部分を定義します

type MyPartial<T> = {
  [K in keyof T]: any;
};

この下に型を見るためにT0という型変数を作って確認してみます。Playground では型変数の下に// ^?とコメントを書くとその型を見ることができます。

type T0 = MyPartial<Person>
//   ^? {
    surname: any;
    middleName?: any;
    givenName: any;
}

次に、anyの部分を元のプロパティの型にします。
インデックスアクセス型を用いると、T[プロパティ名]でプロパティの型が取れるので、以下のように書けます。

type MyPartial<T> = {
  [K in keyof T]: T[K];
};

最後に、全てのプロパティをオプショナルにしたいので?をつけます

type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

これでPartialの実装が完成です!

Pick

型からプロパティキー(文字列リテラルまたは文字列リテラルの和集合)のセットを選択することにより、型を構築します。

type Obj = {
  a: string;
  b: number;
  c: boolean;
};

type T11 = Pick<Obj, "a">;
//   ^? { a: string; }

type T12 = Pick<Obj, "a" | "b">;
//   ^? { a: string; b: number; }

実装に必要な知識

型引数の制約 | TypeScript 入門『サバイバル TypeScript』

型引数の制約

extendsキーワードを使って型引数の型を特定の型に限定することができます

例えば、この例ではTHTMLElementに限定することで、element.style.backgroundColorに安全にアクセスできます。

function changeBackgroundColor<T extends HTMLElement>(element: T) {
  element.style.backgroundColor = "red";
  return element;
}

実装する

Pickの実装はPartialを作った時の知識と、型引数の制約を使って実装することができます。
先ほどと同様に、Playgroundを開いて、以下の箇所を変更して型を実装していきます。

type MyPick<T, K> = any;

実装手順

まずは型引数の制約を用いて、Kの型をTのプロパティ名で限定します。

type MyPick<T, K extends keyof T> = any;

これで、KTのプロパティ名のみを型引数に取ることができるようになりました。

次に、KMapped Typesを用いてプロパティ名としてマップします。

type MyPick<T, K extends keyof T> = {
  [key in K]: any;
};

これで、Kがプロパティ名で、プロパティがanyの型になりました。

最後に、Tのプロパティ名Kのプロパティの型をインデックスアクセスでマップすれば完成です。

type MyPick<T, K extends keyof T> = {
  [key in K]: T[K];
};

実装してみるとわかるのですが、Pickの第二引数にUnion型を渡すと、MappedTypesでマッピングされるので、Unionで渡した全てのプロパティ名を持つ型になることがわかります。

Return Type

関数型の戻り値の型で構成される型を構築します。

type T0 = ReturnType<() => string>;
//   ^ string

実装に必要な知識

関数の型の宣言

type 型の名前 = (引数名: 引数の型) => 戻り値の型;
type Increment = (num: number) => number;

スプレッド演算子

スプレッド演算子は、配列やオブジェクトの要素を展開するのに使います。

function foo(x, y, z) {}
const args = [0, 1, 2];
foo(...args);

Conditional Types

型の条件分岐をすることができます。

type Animal = "dog" | "cat" | "bird";

type IsDog = T extends "dog" ? true : false;

type T0 = IsDog<"dog">;
// ^? true
type T1 = IsDog<"cat">;
// ^? false
type T2 = IsDog<"bird">;
// ^? false

condition ? trueExpression : falseExpression と三項演算子のような表現を使います。

Conditional Type 内での推論

inferを用いると、条件分岐する型の中で、型の推論を行うことができます。
以下の例はTが推論したIの配列であればIを返して、そうでなければTを返す型です。

type Flatten<T> = T extends Array<infer I> ? I : T;

実装する

Playgroundを開いて実装していきます。

今回は関数型の返り値の型を推論したいです。
Conditional Typeを用いて、返り値の型を推論できるようにします。

type MyReturnType<T> = T extends () => infer R ? R : never;

返り値の型を R として、() => any に一致する型ならRを返す型としました。

この状態だと、関数型に引数があった場合にneverとなってしまうので、引数をスプレッド演算で展開して、任意の引数を許可する関数型にします。

type MyReturnType<T> = T extends (...args: Array<any>) => infer R ? R : never;

これでReturnTypeも完成しました。

最後に

型を作ってみるとなんとなく使っていた型について理解が深まったのと、新しい型を作るための勉強になりました。引き続きType Challenges進めてもっと理解を深めていきます。

参考記事

記事内に記載しているリンクを参考記事とさせていただきます。

Discussion