TypeScript には「ある型が条件を満たすかどうかで、返す型を切り替える」という仕組みがあります。これが Conditional Types(条件型)です。A extends B ? X : Y という、三項演算子(条件 ? 真 : 偽)によく似た書き方で、型のレベルで分岐を書けます。この記事では、条件型の基本、型の一部を取り出す infer、ユニオン型への分配、そして標準で用意されている便利な条件型まで、初心者向けに順を追って解説します。
目次
条件型とは何か
条件型は、型を受け取って、条件によって別の型を返す仕組みです。多くの場合、ジェネリクス(型の引数)と組み合わせて使います。基本の形は次のとおりです。
// T が string を継承する(string として扱える)なら "文字列"、そうでなければ "その他" type Check<T> = T extends string ? "文字列" : "その他"; type A = Check<"hello">; // "文字列" type B = Check<123>; // "その他"
T extends string の extends は「継承」ではなく、ここでは「T は string に代入できる型か(string として扱えるか)」という判定を意味します。条件が成り立てば ? の後ろの型、成り立たなければ : の後ろの型になります。JavaScript の三項演算子が値を選ぶのに対し、条件型は型を選ぶ、と考えるとイメージしやすいでしょう。
実用的な例:型によって振る舞いを変える
条件型がよく役立つのは、「渡された型に応じて、返す型を変えたい」場面です。たとえば、配列が渡されたらその要素の型、そうでなければそのままの型を返す、といった処理を型で表現できます。
// 配列ならその中身の型、配列でなければそのままの型 type ElementType<T> = T extends (infer U)[] ? U : T; type N = ElementType<number[]>; // number type S = ElementType<string>; // string(配列ではないのでそのまま)
ここで出てきた infer が、条件型を強力にするもう一つの機能です。次の見出しで詳しく見ていきましょう。
infer で型の一部を取り出す
infer は「条件型の中で、型の一部に名前を付けて取り出す」ためのキーワードです。T extends (infer U)[] は「T が何らかの配列なら、その要素の型を U と名付ける」という意味になります。U は真のブロック(? の後ろ)でだけ使えます。
関数の戻り値の型を取り出す例を見てみましょう。これは実務でもよく使うパターンです。
// 関数型 T の戻り値の型を取り出す
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: 1, name: "太郎" };
}
// typeof で関数の型を取得し、戻り値の型だけを抜き出す
type User = MyReturnType<typeof getUser>; // { id: number; name: string }
infer R で戻り値の型を R として捕まえ、それをそのまま返しています。関数の型を書き換えなくても、そこから必要な部分だけを機械的に取り出せるのが infer の便利なところです。実はこれは、後述する標準の ReturnType とほぼ同じ実装です。
ユニオン型に分配される(distributive)
条件型には、知っておくべき重要な性質があります。裸の型引数(T をそのまま書いたもの)にユニオン型を渡すと、各メンバーごとに条件型が適用され、その結果が再びユニオンにまとめられます。これを「分配(distributive)」と呼びます。
type ToArray<T> = T extends any ? T[] : never; // string | number を渡すと、string と number それぞれに適用される type Result = ToArray<string | number>; // → string[] | number[]((string | number)[] ではない点に注意)
この性質を利用すると、ユニオンから特定の型だけを取り除く、といった操作ができます。次のコードは、ユニオンから null と undefined を除いた型を作ります。
// null と undefined を除外する type NonNull<T> = T extends null | undefined ? never : T; type Value = NonNull<string | null | undefined>; // string
条件の結果に never(値を持たない型)を返すと、その要素はユニオンから消えます。分配のときに never は「何もない」として扱われるため、フィルターのように働くわけです。
標準で用意されている条件型
ここまで自作してきた条件型の多くは、実は TypeScript に標準で組み込まれています(ユーティリティ型と呼ばれます)。自分で書く前に、まず標準のものが使えないか確認しましょう。代表的なものは次のとおりです。
| 型 | 意味 |
|---|---|
Exclude<T, U> | T から U に代入できる型を取り除く |
Extract<T, U> | T から U に代入できる型だけを抜き出す |
NonNullable<T> | T から null と undefined を取り除く |
ReturnType<T> | 関数型 T の戻り値の型を取り出す |
Parameters<T> | 関数型 T の引数の型をタプルで取り出す |
type T1 = Exclude<"a" | "b" | "c", "b">; // "a" | "c"
type T2 = Extract<"a" | "b" | "c", "a">; // "a"
type T3 = NonNullable<string | null>; // string
function add(a: number, b: number) {
return a + b;
}
type T4 = ReturnType<typeof add>; // number
type T5 = Parameters<typeof add>; // [a: number, b: number]
これらはすべて内部的に条件型で作られています。条件型の仕組みを理解しておくと、標準のユーティリティ型が「なぜそう動くのか」も納得できるようになります。
まとめ
条件型は T extends U ? X : Y の形で、型を条件によって切り替える仕組みです。三項演算子の型版と考えると理解しやすく、infer と組み合わせれば型の一部を取り出せます。裸の型引数にユニオンを渡すと各メンバーへ分配される性質を使えば、型のフィルターのような操作も可能です。もっとも、日常的に必要になる操作の多くは Exclude や ReturnType といった標準のユーティリティ型でまかなえます。まずは標準のものを使いこなし、それでは足りないときに自作の条件型を書く——という順序で取り入れていくのがおすすめです。