条件型(Conditional Types)の読み方・書き方

条件型(Conditional Types)は TypeScript の型システムにおける「if 文」だ。型レベルで分岐処理を書けるため、高度なユーティリティ型を定義できる。一見難解だが、基本パターンを押さえれば読み解けるようになる。

基本構文

条件型は三項演算子に似た構文を持つ。

T extends U ? X : Y

TU に代入可能なら X、そうでなければ Y」という意味だ。

type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false
type C = IsString<"hello">;  // true(リテラル型も string に代入可能)

分配条件型

ユニオン型を条件型に渡すと、各メンバーに対して個別に評価される。

type ToArray<T> = T extends any ? T[] : never;

type A = ToArray<string | number>;
// string[] | number[](string と number それぞれに適用される)

これを「分配条件型(Distributive Conditional Types)」と呼ぶ。ユニオン型の各要素に対してマップ的に処理できる。

分配される場合

型パラメータ T をそのまま extends の左辺に置く。ユニオン型の各メンバーに個別に適用される。

分配されない場合

型パラメータを [T] のようにラップする。ユニオン型全体として評価される。

分配を抑制したい場合はこうする。

type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

type B = ToArrayNonDist<string | number>;
// (string | number)[](ユニオン型全体に適用される)

組み込みの条件型

TypeScript が提供するユーティリティ型の多くは条件型で実装されている。

// Exclude: U に代入可能な型を除外
type Exclude<T, U> = T extends U ? never : T;

type A = Exclude<"a" | "b" | "c", "a">;  // "b" | "c"

// Extract: U に代入可能な型だけを抽出
type Extract<T, U> = T extends U ? T : never;

type B = Extract<string | number | boolean, string | number>;  // string | number

// NonNullable: null と undefined を除外
type NonNullable<T> = T extends null | undefined ? never : T;

type C = NonNullable<string | null | undefined>;  // string

ネストした条件型

条件型はネストできる。

type TypeName<T> = 
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

type A = TypeName<string>;  // "string"
type B = TypeName<() => void>;  // "function"
type C = TypeName<{ x: number }>;  // "object"

ただし、深くネストすると可読性が落ちる。適度に型エイリアスで分割しよう。

条件型の読み解き方

複雑な条件型を読むときは、以下のステップで分解する。

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

T が Array<何か> に代入可能か判定

代入可能なら、配列の要素型 U を返す

代入不可能なら、T をそのまま返す

type A = Flatten<string[]>;  // string
type B = Flatten<number>;    // number

よくあるパターン

関数かどうかの判定

type IsFunction<T> = T extends (...args: any[]) => any ? true : false;

type A = IsFunction<() => void>;  // true
type B = IsFunction<string>;      // false

Promise の中身を取り出す

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>;  // string
type B = UnwrapPromise<number>;           // number

オプショナルプロパティの抽出

type OptionalKeys<T> = {
  [K in keyof T]: undefined extends T[K] ? K : never
}[keyof T];

type User = {
  id: string;
  name: string;
  age?: number;
};

type A = OptionalKeys<User>;  // "age"
読み方のコツ

extends を「〜に代入可能か」と読む。三項演算子と同じように「条件 ? 真の場合 : 偽の場合」と解釈する。

書き方のコツ

まず具体的な型でテストし、動作を確認してから一般化する。いきなり複雑な条件型を書こうとしない。

条件型は TypeScript の型パズルの中核だ。infer キーワードと組み合わせるとさらに強力になるが、それは次の記事で詳しく扱う。まずは基本パターンを使いこなせるようになろう。