30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

TypeScript の条件型(Conditional Type)と infer キーワード

TypeScript には条件型(Conditional Type)という機能があり、これを使うと型定義に条件分岐を持ち込むことができる。

この記事の内容は TypeScript のv3.9.5で動作確認している。

条件型の基本

T extends U ? A : Bが、条件型の構文。
TUに割り当て可能なときはA、そうでないときはBを意味する。

そしてtype Example<T> = T extends U ? A : B;のように、ジェネリック型と組み合わせて使う。

ジェネリック型については、以前記事を書いた。

numb86-tech.hatenablog.com

条件型とジェネリック型の組み合わせ例を示す。
以下のIsStringは、渡された型引数Tstringに割り当て可能なときは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 : BTが共用体型のとき、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

ExtractString1 | "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 | neverTになるので、最終結果は"a" | "b"になる。

neverの性質やそれを使ったテクニックについては、下記を参照。

numb86-tech.hatenablog.com

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;