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(...args: infer P) のようにまとめて推論。タプル型として得られる。
(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 完全に近づける強力な機能だ。最初は難解に感じるかもしれないが、標準ライブラリの ReturnType や Parameters の実装を読むところから始めると理解が進む。