Typescript の Utility Types を読み解いてみる
Pick
や Partial
などの Typescript の Utility Types をあまり理解しないまま使っていたので、実際にどのように実装されているのか読み解いてみました。
読み解くために必要な基礎知識
Utility Types で何をやっているか、を読むために最低限必要な知識です。
Keyof Type Operator
型情報からキーを取り出してユニオン型リテラルを生成します。
主に Mapped Types と組み合わせて使われています。
type Point = { x: number; y: number };
type P = keyof Point; // 'x' | 'y' と同じ。
Conditional Types
三項演算子に似た構文が使えます。
SomeType extends OtherType ? TrueType : FalseType;
SomeType
がOtherType
に代入可能であるか、という条件で判別しています。
具体的にはこちら。
type Convert1<T> = T extends null | undefined ? never : T;
type Type1 = Convert1<string | number | null | undefined>; // string | number
type Convert2<T> = T extends null ? boolean : T;
type Type2 = Convert2<string | number | null>; // string | number | boolean
type Convert3<T> = T extends 'en' ? 'fr' : T;
type Type3 = Convert3<'en' | 'ja'>; // 'fr' | 'ja'
Mapped Types
Utility Types の中核機能です。
Mapped Types を使用して再マッピングすることで型を柔軟に変更しています。
type Point = { x: number; y: number };
type BoolType<Point> = {
[Property in keyof Point]: boolean;
};
// プロパティが全て boolean になる。
const bool: BoolType<Point> = {
x: true,
y: false,
};
またユニオン型のマッピングというものも可能で、
Utility Types では、特にユニオン型のマッピングというものをよく使っているようです。
type Union = 'x' | 'y';
type MappedUnion = {
[P in Union]: boolean;
};
const bool2: MappedUnion = {
x: true,
y: false,
};
また、マッピングする際に optional
や readonly
、 required
を指定することもできます。
実際に読み解いてみる
Pick
指定したプロパティだけを持った新しい型を作ることができる Pick
はどのように実装されているのでしょうか。
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
それぞれをバラバラに確認していくと、
-
T
は元になる型です。 -
K
はkey of T
で作られたユニオン型リテラルです。 -
[P in K]
は Kの要素をキーとして再マッピングしています。 -
T[P]
は、指定されたキーの型を渡しています。
K extends keyof T
と指定することで、 T のキーのみを指定可能なユニオン型だけを設定可能にな流ように制限しています。
そしてそのユニオン型をマッピングしているので、 Generics
の2個目に入力したキーだけを含んだ型が作られています。
Readonly/Required/Partial
これらはそれぞれ全てのプロパティを readonly/required/optional
に変更した型を作ります。
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Required<T> = {
[P in keyof T]-?: T[P];
};
type Partial<T> = {
[P in keyof T]?: T[P];
};
それぞれ Mapped Types の機能を利用して readonly
や ?
を再設定しています。
NonNullable/Extract/Exclude
ユニオン型を編集するこれらはどのように実装されているか見ていきましょう。
type NonNullable<T> = T extends null | undefined ? never : T;
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;
NonNullable
は Conditional Types
を使用してユニオン型 T
から null
もしくは undefined
を除外しています。
Exclude
は Conditional Types
を使用してユニオン型 T
から U
で指定された型を除外しています。
Extract
はユニオン型 T
から U
で指定された型以外を除外しています。
Omit
Exclude
はユニオン型だけに作用しましたが、 Omit
はオブジェクト型から指定したキーを排除してくれます。
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Omit
の実装は、Utility Typesを組み合わせて実装されています。
-
Exclude<keyof T, K>
で、TからKを除外したユニオン型リテラルを生成 (NewKeys) -
Pick<T, NewKeys>
でKを除外して再マッピング
疑問点: K の制約の曖昧さ
Omit
の実装では any
が登場しました。これによって、 Omit
の第二引数には制約がありません。
少し違和感あるというか、keyofを使わないのか?という疑問が出てきたので検索してみると同じ疑問を持った人がいましたのでリンクしておきます。
あくまで Utility なので汎用性を重視した結果なのではないかと個人的に思います。
読み解いてみて
実際に読んでみるとこれまで曖昧な理解で使っていた Mapped Type や Conditional Types の理解が進みました。
この経験によって必要な Utility Type を自作することも可能になったので、型定義の重複を極力避けてより快適で堅牢な開発環境を作ることに役立てていきたいと思います。
Discussion