Typescript の Utility Types を読み解いてみる

2022/10/22に公開

https://www.typescriptlang.org/docs/handbook/utility-types.html

PickPartial などの Typescript の Utility Types をあまり理解しないまま使っていたので、実際にどのように実装されているのか読み解いてみました。

読み解くために必要な基礎知識

Utility Types で何をやっているか、を読むために最低限必要な知識です。

Keyof Type Operator

https://www.typescriptlang.org/docs/handbook/2/keyof-types.html

型情報からキーを取り出してユニオン型リテラルを生成します。
主に Mapped Types と組み合わせて使われています。

type Point = { x: number; y: number };
type P = keyof Point; // 'x' | 'y' と同じ。

Conditional Types

https://www.typescriptlang.org/docs/handbook/2/conditional-types.html

三項演算子に似た構文が使えます。

SomeType extends OtherType ? TrueType : FalseType;

SomeTypeOtherTypeに代入可能であるか、という条件で判別しています。
具体的にはこちら。

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

https://www.typescriptlang.org/docs/handbook/2/mapped-types.html

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,
};

また、マッピングする際に optionalreadonlyrequired を指定することもできます。

実際に読み解いてみる

Pick

指定したプロパティだけを持った新しい型を作ることができる Pick はどのように実装されているのでしょうか。

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

それぞれをバラバラに確認していくと、

  • T は元になる型です。
  • Kkey 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;

NonNullableConditional Types を使用してユニオン型 T から null もしくは undefined を除外しています。
ExcludeConditional 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を組み合わせて実装されています。

  1. Exclude<keyof T, K> で、TからKを除外したユニオン型リテラルを生成 (NewKeys)
  2. Pick<T, NewKeys> でKを除外して再マッピング

疑問点: K の制約の曖昧さ

Omit の実装では any が登場しました。これによって、 Omit の第二引数には制約がありません。
少し違和感あるというか、keyofを使わないのか?という疑問が出てきたので検索してみると同じ疑問を持った人がいましたのでリンクしておきます。

https://zenn.dev/jojojo/articles/80f442f27f92e3#omitの問題点&改善案

あくまで Utility なので汎用性を重視した結果なのではないかと個人的に思います。

読み解いてみて

実際に読んでみるとこれまで曖昧な理解で使っていた Mapped Type や Conditional Types の理解が進みました。
この経験によって必要な Utility Type を自作することも可能になったので、型定義の重複を極力避けてより快適で堅牢な開発環境を作ることに役立てていきたいと思います。

Discussion