1型で分岐する最初の一歩
条件型の最も素朴な形です。T extends string が成り立てば "text"、成り立たなければ "not-text" という型に決まります。値の三項演算子とそっくりですが、判定されるのは値ではなく型で、すべてコンパイル時に解決されます。
// T が string に代入できる型なら "text"、そうでなければ "not-text"
type IsText<T> = T extends string ? "text" : "not-text";
// "hello" は string に代入できるので IsText<"hello"> は "text" 型
const answerA: IsText<"hello"> = "text";
// number[] は string に代入できないので "not-text" 型
const answerB: IsText<number[]> = "not-text";
console.log(answerA);
console.log(answerB);
2オブジェクトの形で判定する
extends の右側にはオブジェクト型も書けます。「id: number を持っているか?」のように形で判定できるのがポイントで、継承関係ではなく代入可能かどうかで決まります。プロパティが多い型でも、必要な形を満たしてさえいれば true 側に分岐します。
// 「id: number を持つ形か?」で分岐する
type HasId<T> = T extends { id: number } ? "managed" : "unmanaged";
type Article = { id: number; title: string };
type Draft = { title: string };
// Article は id を持つ(他のプロパティがあってもよい)ので "managed"
const articleState: HasId<Article> = "managed";
// Draft は id を持たないので "unmanaged"
const draftState: HasId<Draft> = "unmanaged";
console.log(articleState);
console.log(draftState);
3条件型を連ねて多分岐にする
条件型の false 側にさらに条件型を書くと、if-else if のような多分岐が作れます。型ごとに表示ラベルを割り当てるような「型から型への対応表」が1つの型定義で書けます。ライブラリの型定義で長く連なった条件型を読むときも、この形が基本です。
// false 側に次の条件型をつなげると多分岐になる
type TypeLabel<T> = T extends string
? "text"
: T extends number
? "count"
: T extends boolean
? "flag"
: "other";
const labelA: TypeLabel<string> = "text";
const labelB: TypeLabel<number> = "count";
const labelC: TypeLabel<true> = "flag";
const labelD: TypeLabel<Date> = "other";
console.log(labelA, labelB, labelC, labelD);
4ユニオンは1候補ずつ分配される
条件型にユニオン型を渡すと、候補1つずつに条件が適用され、結果が再びユニオンにまとめられます。ToArray<string | number> が「string[] | number[]」になるのはこの分配のおかげです。後で学ぶ Exclude などのユーティリティ型もこの挙動を利用しています。
// T extends unknown は常に成立するが、ユニオンを分配するために書く定番の形
type ToArray<T> = T extends unknown ? T[] : never;
// string | number を渡すと、候補ごとに適用されて string[] | number[] になる
type MixedArray = ToArray<string | number>;
// 「文字列だけの配列」も「数値だけの配列」もOK
const wordList: MixedArray = ["a", "b"];
const scoreList: MixedArray = [1, 2, 3];
// const bad: MixedArray = ["a", 1]; // (string | number)[] ではないのでエラー
console.log(wordList);
console.log(scoreList);
5実務: kind に応じて value の型が決まるフォーム項目
フォーム項目の kind が "checkbox" なら value は boolean、"text" なら string、という対応を条件型で1箇所に定義する実務例です。kind と value の食い違いはコンパイルエラーになります。条件型がないと CheckboxField と TextField を別々に手書きして二重管理することになり、項目の種類が増えるたびに型定義が増殖します。
// kind と value の型の対応を1箇所で定義する
type FieldValue<K> = K extends "checkbox" ? boolean : string;
type FormField<K extends "checkbox" | "text"> = {
kind: K;
value: FieldValue<K>;
};
// kind が checkbox なら value は boolean しか入らない
const agreeField: FormField<"checkbox"> = { kind: "checkbox", value: true };
// kind が text なら value は string しか入らない
const emailField: FormField<"text"> = { kind: "text", value: "taro@example.com" };
// const bad: FormField<"checkbox"> = { kind: "checkbox", value: "yes" }; // エラー
console.log(agreeField);
console.log(emailField);