infer キーワードの仕組み

infer は条件型の中で「型を推論して変数に束縛する」ためのキーワードだ。パターンマッチのように、型の一部を取り出して再利用できる。条件型を理解した上で infer を使いこなせると、高度な型操作が可能になる。

基本的な使い方

infer は条件型の extends 節の中でのみ使える。

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

この型は以下のように動く。

T が関数型に代入可能か判定

代入可能なら、戻り値の位置にある型を R として推論

R を返す(代入不可能なら never)

type A = ReturnType<() => string>;  // string
type B = ReturnType<(x: number) => boolean>;  // boolean
type C = ReturnType<string>;  // never(関数型ではない)

infer の位置

infer はパターンの中の任意の位置に置ける。

// 配列の要素型を取り出す
type ElementType<T> = T extends (infer U)[] ? U : never;

type A = ElementType<string[]>;  // string
type B = ElementType<number[]>;  // number
// Promise の中身を取り出す
type Awaited<T> = T extends Promise<infer U> ? U : T;

type C = Awaited<Promise<string>>;  // string
type D = Awaited<Promise<Promise<number>>>;  // Promise<number>

ネストした Promise を再帰的に解決したい場合は、再帰的な型定義が必要になる。

type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;

type E = DeepAwaited<Promise<Promise<Promise<string>>>>;  // string

関数の引数型を取り出す

Parameters<T> の実装を見てみよう。

type Parameters<T> = T extends (...args: infer P) => any ? P : never;

type Params = Parameters<(a: string, b: number) => void>;
// [a: string, b: number]

引数全体をタプル型として P に束縛している。

特定の位置の引数だけを取り出すこともできる。

type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;

type A = FirstArg<(x: string, y: number) => void>;  // string
infer で全体を取り出す

(...args: infer P) のようにまとめて推論。タプル型として得られる。

infer で一部を取り出す

(first: infer F, ...rest: any[]) のように位置を指定して推論。

オブジェクト型での infer

オブジェクトの特定のプロパティの型を取り出すこともできる。

type PropType<T, K extends keyof T> = T extends { [key in K]: infer V } ? V : never;

type User = { name: string; age: number };
type A = PropType<User, "name">;  // string

ただし、これは単に T[K] でもできるので、infer を使う必然性は低い。

複数の infer

条件型の中で複数の infer を使うこともできる。

type FunctionInfo<T> = T extends (arg: infer A) => infer R 
  ? { arg: A; return: R } 
  : never;

type Info = FunctionInfo<(x: string) => number>;
// { arg: string; return: number }

共変・反変と infer

同じ型パラメータが複数の位置で推論される場合、共変・反変のルールが適用される。

type Foo<T> = T extends { a: infer U; b: infer U } ? U : never;

type A = Foo<{ a: string; b: string }>;  // string
type B = Foo<{ a: string; b: number }>;  // string | number(共変位置なのでユニオン)

引数位置(反変位置)で複数回推論される場合は、交差型になる。

type Bar<T> = T extends { f: (x: infer U) => void; g: (x: infer U) => void } ? U : never;

type C = Bar<{ f: (x: string) => void; g: (x: number) => void }>;  
// string & number(反変位置なので交差型)= never
共変位置(戻り値など)

同じ infer U が複数回現れると、ユニオン型(U1 | U2)になる。

反変位置(引数など)

同じ infer U が複数回現れると、交差型(U1 & U2)になる。

実践的なパターン

配列の最初と残りを分離

type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type Rest<T extends any[]> = T extends [any, ...infer R] ? R : never;

type A = First<[1, 2, 3]>;  // 1
type B = Rest<[1, 2, 3]>;   // [2, 3]

文字列リテラルの分解

type Split<S extends string> = S extends `${infer Head}${infer Tail}` 
  ? [Head, ...Split<Tail>] 
  : [];

type A = Split<"abc">;  // ["a", "b", "c"]

テンプレートリテラル型と組み合わせると、文字列パースすら型レベルで実現できる。

infer は TypeScript の型システムを Turing 完全に近づける強力な機能だ。最初は難解に感じるかもしれないが、標準ライブラリの ReturnTypeParameters の実装を読むところから始めると理解が進む。