Kodokon kodokon.com

条件型と infer: コンパイラのロジック

条件型の分配則、infer によるパターンマッチング、そして never・any・boolean といった特殊ケースを使いこなす。

10 分 · 3 問

このレッスンを Kodokon で開く

条件型は型システムにおける分岐の仕組みです。T extends U ? X : Y は、コンパイラに TU に代入可能かどうかを問い、その結果に応じて分岐を選びます。ReturnTypeExcludeAwaited などの標準ユーティリティはすべてこの上に構築されています。このレベルで問われるのは、もはや書き方ではなく、コンパイラがそれをどう評価するかを正確に予測することであり、特にユニオンや never が絡む場合に重要になります。

TYPESCRIPT
type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">;
// true

type B = IsString<string | number>;
// boolean, not false!
ユニオンに対する意外な結果: boolean。

鍵となる挙動は分配則です。テスト対象の型が裸の型パラメータ(タプル・配列・交差型で包まれていないもの)である場合、条件はユニオンの各メンバーに対して個別に適用され、その結果が結合されます。したがって IsString<string | number>true | false、つまり boolean を計算します。この分配を無効化するには、両辺をタプルで包みます: [T] extends [U]。恐るべき特殊ケースが一つあります: never は空のユニオンです。0 個のメンバーに対する分配はどの分岐も評価しないため、どちらの分岐を書いたかにかかわらず結果は never になります。

TYPESCRIPT
type ToArray<T> = T extends unknown ? T[] : never;
type NoDist<T> = [T] extends [unknown] ? T[] : never;

type A = ToArray<string | number>;
// string[] | number[]

type B = NoDist<string | number>;
// (string | number)[]

type IsNever<T> = [T] extends [never] ? true : false;
type C = IsNever<never>; // true thanks to the tuple
タプルが分配を打ち消し、never を信頼できるものにする。

infer キーワードは、パターンマッチングを通じて捕捉される型変数を導入します。コンパイラはテスト対象の型の構造をパターンと照合しようとし、マッチが成功すればその変数を束縛します。同じ名前で複数の infer 宣言を置くこともできます。共変の位置(戻り値の型など、生成される値)では、候補はユニオンにまとめられ、反変の位置(関数の引数)では交差型にまとめられます。この規則は変性から直接導かれます。それぞれの方向で唯一安全な選択だからです。

TYPESCRIPT
type ElementOf<T> =
  T extends readonly (infer E)[] ? E : never;

type FirstParam<T> =
  T extends (arg: infer P) => unknown ? P : never;

type Merged<T> =
  T extends { a: (x: infer P) => void;
              b: (x: infer P) => void }
    ? P : never;

type M = Merged<{
  a: (x: { id: string }) => void;
  b: (x: { tag: string }) => void;
}>; // { id: string } & { tag: string }
反変の位置にある 2 つの infer: 交差型。

TypeScript 4.7 以降、infer はインラインの制約を受け付けます: infer N extends number は捕捉をフィルタリングし、4.8 以降ではコンパイラが数値の文字列リテラルを数値リテラルへ変換までしてくれます。もう一つの仕様上の細かな点: 入力にまだ解決されていないジェネリックパラメータを含む条件型は遅延されます。それはインスタンス化の箇所でのみ評価されるため、一部のエラーが呼び出し側でしか現れない理由がここにあります。

TYPESCRIPT
type ToNumber<S extends string> =
  S extends `${infer N extends number}` ? N : never;

type Port = ToNumber<"8080">; // the literal 8080
type Bad = ToNumber<"abc">;   // never
infer への制約とリテラル変換 (4.8)。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. Exclude<boolean, true> は何になりますか?
    • boolean
    • false
    • never
    • true
  2. type IsNever<T> = T extends never ? true : false; において、IsNever<never> は何になりますか?
    • true
    • false
    • never
    • boolean
  3. 関数の引数の位置に置かれた、同じ名前の 2 つの infer P が生成するのは:
    • 候補のユニオン
    • 候補の交差型
    • 最初に出会った候補
    • コンパイルエラー