Menu

TypeScriptの条件型(Conditional Types)と infer

条件型は、テストに基づいて2つの型のどちらかを選びます: T extends U ? X : Y。構文、条件型がユニオン型に分配される仕組み(とその止め方)、infer による型の取り出し、ReturnType を自分で書く方法を解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

条件型は、JavaScriptの三項演算子のように読めるテストで2つの型のどちらかを選びます: T extends U ? X : Y。T が U に代入できれば結果は X、そうでなければ Y です。

ここでの extends は「代入できる」という意味で、値を変数に代入するときにコンパイラーが使うのと同じ関係です。条件型はコンパイル時にしか存在せず、JavaScriptの出力からは消去されます。

構文

type Result = CheckedType extends TestType ? TrueType : FalseType;

条件型が役に立つのはジェネリクスと組み合わせたときです。チェックする型は型パラメーターで、あとで具体的な型が入ります。true のブランチでは、チェックした型がテストに一致することをTypeScriptが知っているので、普通の T には message プロパティがないのに、上の T["message"] が許されます。

三項演算子の連鎖のようにネストもできます:

関数の本体の as TypeName<T> は省略できません。その理由は次のセクションで説明します。

条件型の戻り値にはアサーションが必要

自分の型パラメーターに対する条件型を戻り値の型に持つ関数は、どちらのブランチも直接は返せません。TypeScriptは本体の中で T を絞り込まないので、どのブランチが当てはまるかわからないからです:

index.ts(5,34): error TS2322: Type 'number' is not assignable to type 'Flip<T>'.
index.ts(5,45): error TS2322: Type 'string' is not assignable to type 'Flip<T>'.

よくある直し方は2つあります。入力と出力の組をそれぞれ明示して呼び出し側を正確にチェックするオーバーロードか、実装の中でのアサーションです。

オーバーロードについては 関数のオーバーロードのページ で扱っています。アサーションを使うとコンパイラーはあなたを信用するので、本体で間違ったブランチを返しても捕まえられません。

分配条件型

チェックする型がむき出しの型パラメーターで、ユニオン型を受け取ると、条件はメンバーごとに1回ずつ実行され、結果が新しいユニオン型に結合されます:

Exclude と Extract を動かしているのが分配です。Exclude<T, U> は T extends U ? never : T と定義されています。U に一致する各メンバーは never になり、never はユニオン型から消えます。そのため Exclude<"a" | "b" | "c", "a"> は "b" | "c" です。

同じルールから意外な結果が2つ生まれます。boolean はユニオン型 true | false なので、ToArray<boolean> は boolean[] ではなく false[] | true[] です。そして never は空のユニオン型なので、never を与えられた分配条件型は何もテストせずに never を返します:

type IsNever<T> = T extends never ? true : false;
type X = IsNever<never>; // never, not true

type IsNeverFixed<T> = [T] extends [never] ? true : false;
type Y = IsNeverFixed<never>; // true

infer で型を取り出す

infer は extends 句の中で新しい型変数を宣言します。一致すれば、TypeScriptはチェックした型からその変数を埋め、true のブランチでそれを使えます:

T extends Promise<infer V> ? V : T は「T が何かの Promise なら、その何かを V と呼んでそれを返す。そうでなければ T をそのまま返す」と読みます。infer を使えるのは条件型の extends 句の中だけです。

infer の変数には extends で独自の制約を付けられます。その場合、推論された型が制約に合うときだけ一致します:

type FirstString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;

type A = FirstString<["a", 1]>; // "a"
type B = FirstString<[1, "a"]>; // never: the first element is not a string

ReturnType を自分で作る

組み込みの ReturnType は、infer を使った1行の条件型です。自分で書いてみるのは、2つの考え方を腑に落とす定番の練習です:

typeof makeUser が関数の値を型に変え、条件型がそれを「任意の関数」と照らし合わせて、戻り値の型を R として捉えます。標準ライブラリの版は2点が違います。型パラメーターが関数型に制約されている(T extends (...args: any) => any)ので、ReturnType<string> は never ではなくコンパイルエラーになり、false のブランチは any です。Parameters、InstanceType、Awaited も ReturnType のページで扱っています。

再帰的な条件型

条件型は自分自身を参照でき、任意の深さのネストを展開できます:

type Flatten<T> = T extends readonly (infer U)[] ? Flatten<U> : T;

type A = Flatten<number[][][]>; // number
type B = Flatten<string>;       // string

組み込みの Awaited<T> はこの方法で Promise<Promise<T>> を T まで展開しています。実際には再帰は浅く保ちましょう。非常に深い再帰や終わりのない再帰では、コンパイラーは error TS2589: Type instantiation is excessively deep and possibly infinite で諦めてしまいます。

クイックリファレンス

パターン意味
T extends U ? X : YT が U に代入できれば X、そうでなければ Y
T extends U ? never : TU に一致するメンバーを取り除く(これが Exclude)
T extends U ? T : neverU に一致するメンバーを残す(これが Extract)
[T] extends [U] ? X : Y同じテストを、ユニオン型に分配せずに行う
T extends (infer E)[] ? E : T配列の要素の型
T extends Promise<infer V> ? V : TPromise の値の型
T extends (...args: any[]) => infer R ? R : never関数の戻り値の型
T extends [infer H, ...infer Rest] ? ...タプルの最初の要素と残り

よくある質問

TypeScriptの条件型とは何ですか?

T extends U ? X : Y という形の型です。T が U に代入できれば結果は X、そうでなければ Y です。型のための if/else で、コンパイル時に評価され、出力されるJavaScriptには何も残りません。

TypeScriptの infer キーワードは何をしますか?

infer は条件型の extends 句の中で型変数を宣言し、一致した型からTypeScriptにそれを埋めさせます。T extends Promise<infer V> ? V : T は、Promise の型から解決される値の型を取り出します。使えるのは条件型の extends 句の中だけです。

分配条件型とは何ですか?

チェックする型がむき出しの型パラメーターで、そこにユニオン型を渡すと、条件は各メンバーに別々に適用され、結果が結合されます。type ToArray<T> = T extends unknown ? T[] : never に対する ToArray<string | number> は string[] | number[] になります。これを止めるには両辺をかっこで囲みます: [T] extends [unknown]。

TypeScriptで関数の戻り値の型を取得するには?

組み込みの ReturnType<typeof fn> を使います。これは infer を使った条件型です: T extends (...args: any) => infer R ? R : any。async 関数なら、Promise ではなく解決される値を得るために Awaited<...> で囲みます。

IsNever<never> が true ではなく never を返すのはなぜですか?

never は空のユニオン型で、分配条件型はユニオン型のメンバーに対してマップするからです。メンバーがなければマップするものがなく、結果は never になります。never そのものをテストするには [T] extends [never] ? true : false と書きます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める