【TypeScript】Conditional Typeで型の条件分岐を行う
どうもフロントエンドエンジニアのoreoです。
今回は、型の条件分岐ができるConditional Typeについて整理します。
1 Conditional Typeについて
1-1 Conditional Type
Conditional Typeは、「条件型」とも呼ばれ、T extends U ? X : Yの様な形をとります。三項演算子みたいなもので、TがUのサブタイプである場合はXを返し、そうでない場合は、Yを返します。
例えば、👇のIsNumberの様に、Generics型であるTが、number型のサブタイプであれば、booleanリテラル型のtrueを返し、それ以外の場合はfalseを返します
type IsNumber<T> = T extends number ? true : false;
type T1 = IsNumber<10>;       //type T1 = true
type T2 = IsNumber<"テスト">;  //type T2 = false
type T3 = IsNumber<number>;   //type T3 = true
<T>のようようなGenerics型に関しては、こちらの記事をご覧ください。
1-2 Union distribution(ユニオンの分配)
T extends U ? X : Yにおいて、Tが型変数かつユニオン型の場合、Union distributionという挙動が発生します。👇のように、TがT1 | T2 | T3などのユニオン型の場合、ユニオン型のそれぞれの要素に対して、Conditional Typeが適用されます。
(T1 | T2 | T3) extends U ? X : Y
//Union distributionにより、👇と同じになります。
(T1 extends U ? X : Y) | (T2 extends U ? X : Y) | (T3 extends U ? X : Y)
Union distributionは、ユーティリティ型のExtractなどで使用されています。Extractは、👇のMyExtractのように定義できます。
//ユーティリティ型のExtractを、MyExtractとして定義してみる
type MyExtract<T, U> = T extends U ? T : never;
MyExtractを使用すると、ユニオン型のJobGradeから、型SuperJobGradeとして、"S"と"A"を抽出することが可能です。
type JobGrade = "S" | "A" | "B";
type SuperJobGrade = MyExtract<JobGrade, "S" | "A">; //type SuperJobGrade = "S" | "A"
この場合、MyExtractでは、👇のように、Union distributionが発生しています。
("S" extends "S" | "A" ? "S" : never) | ("A" extends "S" | "A" ? "A" : never) | ("B" extends "S" | "A" ? "B" : never)
こちらのUnion distributionは、条件部分が型変数の場合のみ発生する点、注意ください(👇参考)。
1-3 infer
inferは、Conditional TypeT extends U ? X : YのUの部分で使うキーワードで、Uの型の条件とマッチした型を抽出し、それをX、Yで利用することができます。
inferは、ユーティリティ型のReturnTypeなどで使用されています。ReturnTypeは、👇のMyReturnTypeのように定義できます。
//ユーティリティ型のReturnTypeを、MyReturnTypeとして定義してみる
type MyReturnType<T> = T extends (...args:any[])=> infer R ? R : never
MyReturnTypeでは、Uの部分に関数型(...args:any[])=> infer Rが定義されています。この場合、Tに渡された型が、関数型の場合、その関数型の返り値の型をinferがRとして抽出してくれます。
// () => number の返り値の型numberを抽出
type Num = MyReturnType<() => number>;           //type Num = number
// (x: string) => string の返り値の型stringを抽出
type Str = MyReturnType<(x: string) => string>;  //type Str = string
// (a: boolean, b: boolean) => boolean[] の返り値の型boolean[]を抽出
type Bools = MyReturnType<(a: boolean, b: boolean) => boolean[]>;  //type Bools = boolean[]
2 最後に
Union distributionやinferに関しては、codeを読むだけでは、何が起きているのか全くわかりませんでしたが、一度その挙動を知ると便利ですね。自作のユーティリティ型を作る時に大活躍しそうです!
3 参考
Documentation - Conditional Types
TypeScript の条件型(Conditional Type)と infer キーワード - 30歳からのプログラミング

Discussion
Union distribution の説明ですが、以下で X1 が
booleanではなくfalseになるので間違えていませんか…?これっぽいです
pandanoir様
ご指摘ありがとうございます。また、誤認しており大変申し訳ありませんでした、、!Union distribution部分を修正させていただきました!
このようなご指摘いただけるのは大変嬉しいです、ありがとうございます!!
お疲れ様です! 有益な記事を書いて頂き、ありがとうございます🙏
コード例を読んでいたのですが、一点、誤りと思われる箇所を発見したのでご連絡致します。
となっているのですが、これは
が正しい形だと思われます。
type MyExtract<T, U> = T extends U ? T : never;と型定義をしているので、"A"のときは"A" : neverになり、"B"のときは"B" : neverになるからです。ご確認頂けたら嬉しいです✨
LEF様
typoしておりました、、修正しております!
このようなご指摘いただけるのは大変嬉しいです、ありがとうございます!!