🔍

【TypeScript】Conditional Typeで型の条件分岐を行う

2022/05/16に公開
5

どうもフロントエンドエンジニアのoreoです。

今回は、型の条件分岐ができるConditional Typeについて整理します。

1 Conditional Typeについて

1-1 Conditional Type

Conditional Typeは、「条件型」とも呼ばれ、T extends U ? X : Yの様な形をとります。三項演算子みたいなもので、TUのサブタイプである場合は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という挙動が発生します。👇のように、TT1 | 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は、条件部分が型変数の場合のみ発生する点、注意ください(👇参考)。

https://qiita.com/uhyo/items/da21e2b3c10c8a03952f#分配されるのは型変数のみ

1-3 infer

inferは、Conditional TypeT extends U ? X : YUの部分で使うキーワードで、Uの型の条件とマッチした型を抽出し、それをXYで利用することができます。

inferは、ユーティリティ型のReturnTypeなどで使用されています。ReturnTypeは、👇のMyReturnTypeのように定義できます。

//ユーティリティ型のReturnTypeを、MyReturnTypeとして定義してみる
type MyReturnType<T> = T extends (...args:any[])=> infer R ? R : never

MyReturnTypeでは、Uの部分に関数型(...args:any[])=> infer Rが定義されています。この場合、Tに渡された型が、関数型の場合、その関数型の返り値の型をinferRとして抽出してくれます。

// () => 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 になるので間違えていませんか…?

type X1 = string | number extends string ? true:false;
type X2 = (string extends string ? true : false) | (number extends string ? true : false);
oreo2990oreo2990

pandanoir様
ご指摘ありがとうございます。また、誤認しており大変申し訳ありませんでした、、!Union distribution部分を修正させていただきました!

このようなご指摘いただけるのは大変嬉しいです、ありがとうございます!!

LEFLEF

お疲れ様です! 有益な記事を書いて頂き、ありがとうございます🙏

コード例を読んでいたのですが、一点、誤りと思われる箇所を発見したのでご連絡致します。

("S" extends "S" | "A" ? "S" : never) | ("A" extends "S" | "A" ? "S" : never) | ("B" extends "S" | "A" ? "S" : never)

となっているのですが、これは

("S" extends "S" | "A" ? "S" : never) | ("A" extends "S" | "A" ? "A" : never) | ("B" extends "S" | "A" ? "B" : never)

が正しい形だと思われます。

type MyExtract<T, U> = T extends U ? T : never;と型定義をしているので、"A"のときは"A" : neverになり、"B"のときは"B" : neverになるからです。

ご確認頂けたら嬉しいです✨

oreo2990oreo2990

LEF様
typoしておりました、、修正しております!
このようなご指摘いただけるのは大変嬉しいです、ありがとうございます!!