TypeScript には条件型(Conditional Type)という機能があり、これを使うと型定義に条件分岐を持ち込むことができる。
この記事の内容は TypeScript のv3.9.5で動作確認している。
条件型の基本
T extends U ? A : Bが、条件型の構文。
TがUに割り当て可能なときはA、そうでないときはBを意味する。
そしてtype Example<T> = T extends U ? A : B;のように、ジェネリック型と組み合わせて使う。
ジェネリック型については、以前記事を書いた。
条件型とジェネリック型の組み合わせ例を示す。
以下のIsStringは、渡された型引数Tがstringに割り当て可能なときはtrueを、不可能なときはfalseを返す。
type IsString<T> = T extends string ? true : false; type A = IsString<string>; // type A = true type B = IsString<"a">; // type B = true type C = IsString<number>; // type C = false
分配法則
条件型は、分配法則に従う。
そのため、T extends U ? A : BのTが共用体型のとき、Tのそれぞれの要素に対して条件型が展開される。
T1 | T2 extends U ? A : B // ↓ (T1 extends U ? A : B) | (T2 extends U ? A : B)
この性質を使うことで、共用体型の中から特定の条件を満たした要素のみを取り出す、ということが可能になる。
例えば以下のExtractStringは、渡された値の中から、string及びそのサブタイプを取り出す。
type ExtractString<T> = T extends string ? T : never; type A = ExtractString<1 | "a" | "b">; // type A = "a" | "b" type B = ExtractString<1 | 2>; // type B = never
ExtractStringに1 | "a" | "b"が渡された場合、以下のように計算が進む。
ExtractString<1 | "a" | "b"> // ↓ 1 | "a" | "b" extends string ? T : never // ↓ (1 extends string ? T : never) | ("a" extends string ? T : never) | ("b" extends string ? T : never) // ↓ never | "a" | "b"
neverの性質上、T | neverはTになるので、最終結果は"a" | "b"になる。
neverの性質やそれを使ったテクニックについては、下記を参照。
TypeScript の組み込みの条件型でも、このテクニックがよく使われている。
/** * Exclude from T those types that are assignable to U */ type Exclude<T, U> = T extends U ? never : T; /** * Extract from T those types that are assignable to U */ type Extract<T, U> = T extends U ? T : never; /** * Exclude null and undefined from T */ type NonNullable<T> = T extends null | undefined ? never : T;
infer キーワードによるキャプチャ
inferキーワードを使うと、条件にマッチした際にその一部分をキャプチャして利用することが可能になる。
T extends U ? A : Bの、Uの部分でinferを使う。
そうすると、条件にマッチしたときにその型をキャプチャし、それをAの部分で使えるようになる。
type ResolvedType<T> = T extends Promise<infer X> ? X : never; type A = ResolvedType<Promise<1>>; // type A = 1 type B = ResolvedType<Promise<string>>; // type B = string type C = ResolvedType<1>; // type C = never
inferは、ジェネリック型のようなものだと考えることができる。上記のResolvedTypeの定義で使っているXは、定義した時点では型が決まっていない。
ResolvedTypeに型引数が渡されると TypeScript は、その型をTにバインドする。そしてその型に応じて、Xが何であるかを推論するのである。
どのような型もXにバインドできない場合は、条件にマッチしなかったと判定され、neverが返される。
Xに具体的な型をバインドできた場合は条件にマッチしたと判定され、Xが返される。
inferも、組み込みの条件型で使われている。
以下は、関数の返り値の型を返すReturnTypeの例。
/** * Obtain the return type of a function type */ type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;