条件型(Conditional Types)の読み方・書き方
条件型(Conditional Types)は TypeScript の型システムにおける「if 文」だ。型レベルで分岐処理を書けるため、高度なユーティリティ型を定義できる。一見難解だが、基本パターンを押さえれば読み解けるようになる。
基本構文
条件型は三項演算子に似た構文を持つ。
T extends U ? X : Y「T が U に代入可能なら 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>; // falsePromise の中身を取り出す
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 キーワードと組み合わせるとさらに強力になるが、それは次の記事で詳しく扱う。まずは基本パターンを使いこなせるようになろう。