Type Challengesを始めよう!
こんにちは
フロントエンドエンジニアのぴです
最近会社の他の社員の方と、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 型演算子 | TypeScript 入門『サバイバル TypeScript』
- インデックス型 (index signature) | TypeScript 入門『サバイバル TypeScript』
- Mapped Types | TypeScript 入門『サバイバル TypeScript』
- インデックスアクセス型 (indexed access types) | TypeScript 入門『サバイバル TypeScript』
それぞれ詳しくみていきます
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 Type
とkeyof
型演算子、インデックス型を組み合わせて、プロパティ名の部分を定義します
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
キーワードを使って型引数の型を特定の型に限定することができます
例えば、この例ではT
をHTMLElement
に限定することで、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;
これで、K
はT
のプロパティ名のみを型引数に取ることができるようになりました。
次に、K
をMapped 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
実装に必要な知識
- 関数の型の宣言 (function type declaration) | TypeScript 入門『サバイバル TypeScript』
- スプレッド演算子 - TypeScript Deep Dive 日本語版
- TypeScript: Documentation - Conditional Types
関数の型の宣言
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