inferが必要になる場面 — 型情報を動的に取り出す課題

TypeScriptで関数やPromise、配列などを扱うとき、「この関数の戻り値の型だけ取り出したい」「Promiseの中身の型を参照したい」という場面は頻繁に発生します。

function fetchUser() {
  return { id: 1, name: "Alice", email: "alice@example.com" };
}

// fetchUserの戻り値の型を別の変数に使いたい
// 手動で型を定義すると、関数の変更に追従できない
type User = { id: number; name: string; email: string }; // 二重管理になる

このような「既存の型の一部を自動的に抽出する」処理を実現するのが infer キーワードです。TypeScript 2.8(2018年3月リリース)で条件型(Conditional Types)とともに導入されました(出典: TypeScript 2.8 リリースノート)。

infer を使えば、型の構造に対してパターンマッチを行い、一致した部分を型変数として取り出せます。

// inferを使えば関数の変更に自動追従する
type UserFromFn = ReturnType<typeof fetchUser>;
// { id: number; name: string; email: string }

inferの構文ルールと動作原理

条件型(Conditional Types)の前提知識

infer は条件型の内部でのみ動作します。条件型は三項演算子に似た構文で、型レベルの分岐を表現します。

// 基本構文
type Check<T> = T extends string ? "文字列です" : "文字列ではありません";

type A = Check<string>;  // "文字列です"
type B = Check<number>;  // "文字列ではありません"

T extends U ? X : Y は「TがUに代入可能なら型X、そうでなければ型Y」と読みます。

inferの配置ルール — extends右辺で型変数を宣言する

infer は条件型の extends 句の右辺に配置し、型のパターンマッチで抽出したい部分を宣言します。

type ExtractReturn<T> = T extends (...args: any[]) => infer R ? R : never;
//                                                    ^^^^^^^
//                                     ここでRという型変数を宣言(キャプチャ)

ルールをまとめると次のとおりです。

ルール内容
配置可能な場所条件型の extends 句の右辺のみ
型変数の参照範囲true分岐(? の直後)でのみ使用可能
false分岐での参照不可(コンパイルエラー)
複数のinfer宣言同一条件型内で複数の infer を使用可能

inferが使えない場所とエラー例

infer を条件型の外で使おうとするとコンパイルエラーになります。

// NG: extends句の外では使用不可
type Bad = infer T;
// エラー: 'infer' declarations are only permitted in the 'extends' clause of a conditional type.

// NG: false分岐では参照不可
type AlsoBad<T> = T extends Array<infer U> ? string : U;
// エラー: Cannot find name 'U'.

基本パターン — inferで型の一部を抽出する6つの手法

関数の戻り値型を取り出す

最も代表的なユースケースです。関数型の戻り値部分を infer R でキャプチャします。

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

// 使用例
function createUser(name: string, age: number) {
  return { name, age, createdAt: new Date() };
}

type User = MyReturnType<typeof createUser>;
// { name: string; age: number; createdAt: Date }

関数の引数型を取り出す

引数部分を infer P でキャプチャすると、タプル型として取得できます。

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

type Params = MyParameters<typeof createUser>;
// [name: string, age: number]

// 個別の引数を取り出す
type FirstArg = Params[0]; // string
type SecondArg = Params[1]; // number

Promiseの解決値を取り出す

非同期処理の結果型を抽出するパターンです。

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

type A = UnwrapPromise<Promise<string>>;  // string
type B = UnwrapPromise<Promise<number[]>>; // number[]
type C = UnwrapPromise<boolean>;           // boolean(Promiseでなければそのまま)

ネストしたPromiseを再帰的に展開する場合は、TypeScript 4.5で追加された組み込みの Awaited<T> を使うのが適切です。

type Deep = Awaited<Promise<Promise<Promise<string>>>>;
// string(何段ネストしても展開される)

配列・タプルの要素型を取り出す

配列の要素型や、タプルの特定位置の型を抽出できます。

// 配列の要素型
type ElementOf<T> = T extends (infer E)[] ? E : never;

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

// タプルの先頭要素(TypeScript 4.0+のVariadic Tuple Types)
type Head<T extends any[]> = T extends [infer F, ...any[]] ? F : never;

// タプルの末尾要素
type Tail<T extends any[]> = T extends [...any[], infer L] ? L : never;

type F = Head<[string, number, boolean]>; // string
type L = Tail<[string, number, boolean]>; // boolean

コンストラクタの引数型・インスタンス型を取り出す

クラスのコンストラクタに対しても infer でパターンマッチが可能です。

// コンストラクタ引数の型
type MyConstructorParams<T> = T extends new (...args: infer P) => any ? P : never;

// インスタンスの型
type MyInstanceType<T> = T extends new (...args: any[]) => infer R ? R : never;

class Article {
  constructor(public title: string, public body: string, public publishedAt: Date) {}
}

type Args = MyConstructorParams<typeof Article>;
// [title: string, body: string, publishedAt: Date]

type Instance = MyInstanceType<typeof Article>;
// Article

テンプレートリテラル型から部分文字列を抽出する

TypeScript 4.1以降、テンプレートリテラル型内で infer を使った文字列パターンマッチが可能です。

// URLパスからパラメータ名を抽出
type ExtractParam<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParam<Rest>
    : T extends `${string}:${infer Param}`
      ? Param
      : never;

type RouteParams = ExtractParam<"/users/:userId/posts/:postId">;
// "userId" | "postId"
// イベント名から操作部分を抽出
type ExtractAction<T extends string> =
  T extends `on${infer Action}` ? Action : never;

type A = ExtractAction<"onClick">;   // "Click"
type B = ExtractAction<"onSubmit">;  // "Submit"
type C = ExtractAction<"reset">;     // never(パターン不一致)

組み込みユーティリティ型の内部実装とinfer

TypeScriptの標準ライブラリ(lib.es5.d.ts)には、infer を使って定義されたユーティリティ型が複数含まれています。それぞれの実装を理解すると、自作のユーティリティ型を設計するときの参考になります。

ユーティリティ型内部実装抽出対象
ReturnType<T>T extends (...args: any) => infer R ? R : any関数の戻り値型
Parameters<T>T extends (...args: infer P) => any ? P : never関数の引数型(タプル)
ConstructorParameters<T>T extends abstract new (...args: infer P) => any ? P : neverコンストラクタ引数型
InstanceType<T>T extends abstract new (...args: any) => infer R ? R : anyインスタンス型
Awaited<T>再帰的にPromiseを展開(内部で infer 使用)Promiseの解決値

出典: TypeScript Handbook - Utility Types

これらはすべて同じパターンで構成されています。「型 T が特定の構造に一致するか条件型で検査し、一致した部分を infer で取り出す」という手法です。


infer extendsで推論結果に制約を付ける(TypeScript 4.7以降)

TypeScript 4.7で導入された infer ... extends 構文を使うと、推論結果に型の制約を直接付与できます(出典: TypeScript 4.7 アナウンス)。

従来の書き方(ネストが深くなる)

// 配列の先頭が文字列の場合のみ取り出す(TS 4.6以前)
type FirstIfString_Old<T> =
  T extends [infer S, ...unknown[]]
    ? S extends string
      ? S
      : never
    : never;

infer extends を使った書き方(TS 4.7+)

// 制約をinferに直接付与できる
type FirstIfString<T> =
  T extends [infer S extends string, ...unknown[]]
    ? S
    : never;

type A = FirstIfString<["hello", 42]>;   // "hello"
type B = FirstIfString<[42, "hello"]>;    // never

条件型のネストが1段減るため、可読性が大きく向上します。

テンプレートリテラルとの組み合わせ(TypeScript 4.8+)

TypeScript 4.8以降では、テンプレートリテラル型内の infer extends でリテラル型が推論されるようになりました(出典: TypeScript 4.8 アナウンス)。

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

type A = ToNumber<"42">;   // 42(リテラル型)
type B = ToNumber<"100">;  // 100(リテラル型)

type ToBool<T extends string> =
  T extends `${infer B extends boolean}` ? B : never;

type C = ToBool<"true">;  // true(リテラル型)
type D = ToBool<"false">; // false(リテラル型)

この機能により、文字列リテラルから数値や真偽値のリテラル型への変換が型レベルで実現できます。


実務で役立つinfer応用パターン

APIレスポンスの型からデータ部分を抽出する

REST APIのレスポンスが共通のラッパー構造を持つ場合、データ部分だけを取り出せます。

// APIレスポンスの共通構造
type ApiResponse<T> = {
  status: number;
  message: string;
  data: T;
};

// data部分の型を抽出
type ExtractData<T> = T extends ApiResponse<infer D> ? D : never;

// 具体例
type UserResponse = ApiResponse<{ id: number; name: string }>;
type UserData = ExtractData<UserResponse>;
// { id: number; name: string }

イベントハンドラの引数型を推論する

Reactなどのフレームワークでイベントハンドラを扱うとき、コールバック関数の引数型を抽出できます。

type EventMap = {
  click: { x: number; y: number };
  keydown: { key: string; code: string };
  scroll: { scrollTop: number };
};

type HandlerArg<K extends keyof EventMap> = EventMap[K];

// 複雑なハンドラ型から引数を抽出するパターン
type ExtractHandlerPayload<T> =
  T extends (event: infer E) => void ? E : never;

type ClickHandler = (event: { x: number; y: number; target: Element }) => void;
type Payload = ExtractHandlerPayload<ClickHandler>;
// { x: number; y: number; target: Element }

Union型からIntersection型への変換テクニック

やや高度な応用ですが、infer の反変位置(contravariant position)での挙動を利用すると、Union型をIntersection型に変換できます。

type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends (k: infer I) => void
    ? I
    : never;

type Result = UnionToIntersection<{ a: 1 } | { b: 2 } | { c: 3 }>;
// { a: 1 } & { b: 2 } & { c: 3 }

このテクニックは、同名の infer が反変位置に複数回出現するとインターセクション型に推論されるというTypeScriptの仕様を利用しています。

再帰型でネスト構造を展開する

infer と再帰型を組み合わせると、深くネストした型を再帰的に処理できます。

// ネストしたオブジェクトを再帰的にReadonlyにする
type DeepReadonly<T> = T extends (...args: any[]) => any
  ? T
  : T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T;

// ネストしたPromiseを完全に展開する
type DeepUnwrapPromise<T> = T extends Promise<infer U>
  ? DeepUnwrapPromise<U>
  : T;

type A = DeepUnwrapPromise<Promise<Promise<Promise<string>>>>;
// string

inferのよくある落とし穴とデバッグ手法

推論結果がneverになるケース

infer によるパターンマッチが失敗すると、false分岐に入って never が返ります。意図せず never になる主な原因は次のとおりです。

// 原因1: extends句のパターンが合っていない
type Bad1<T> = T extends (infer U)[] ? U : never;
type R1 = Bad1<string>; // never(stringは配列ではない)

// 原因2: 引数の型制約が不適切
type Bad2<T extends string> = T extends `prefix_${infer Rest}` ? Rest : never;
type R2 = Bad2<"other_value">; // never(prefixで始まっていない)

対処法として、false分岐で元の型 T をそのまま返す設計にすると、パターン不一致時にも型情報が失われません。

type SafeUnwrap<T> = T extends Promise<infer U> ? U : T;
// Promiseでない型を渡してもTがそのまま返る

複数のinfer変数が同名のときの挙動

同名の infer が複数出現した場合、その位置が共変(covariant)か反変(contravariant)かで結果が変わります。

// 共変位置(戻り値型)→ Union型になる
type CovariantExample<T> =
  T extends { a: infer U; b: infer U } ? U : never;

type R1 = CovariantExample<{ a: string; b: number }>;
// string | number(ユニオン型)

// 反変位置(引数型)→ Intersection型になる
type ContravariantExample<T> =
  T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never;

type R2 = ContravariantExample<{ a: (x: string) => void; b: (x: number) => void }>;
// string & number(= never、stringとnumberの交差は存在しない)

分配的条件型との組み合わせで起きる意外な結果

条件型の T にUnion型が入ると、各メンバーに分配(distribute)されます。infer を使う条件型でも分配が起きることに注意が必要です。

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

// Union型を渡すと分配される
type Result = UnwrapArray<string[] | number>;
// string | number
// ↑ string[]に対してstringが返り、numberに対してnumberが返り、結果がUnionになる

分配を防ぎたい場合は、T をタプルで囲みます。

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

type Result = UnwrapArrayNoDistribute<string[] | number>;
// string[] | number(分配されずにそのまま判定される)

オーバーロード関数に対するinferの制約

オーバーロードされた関数型に ReturnTypeParameters を適用すると、最後のシグネチャのみが推論対象になります。

declare function overloaded(x: string): string;
declare function overloaded(x: number): number;

type R = ReturnType<typeof overloaded>;
// number(最後のシグネチャの戻り値型)

この挙動はTypeScriptの仕様によるものであり、全シグネチャをUnion型で取得する手段は標準では提供されていません。


inferの使いどころ判断ガイド

infer は強力ですが、すべての場面で必要になるわけではありません。以下の基準で使いどころを判断できます。

inferを使うべき場面:

  • ライブラリやフレームワークが公開する型から、内部の型を取り出したいとき
  • 汎用的なユーティリティ型を自作するとき
  • 関数の型シグネチャから引数型や戻り値型を導出したいとき
  • テンプレートリテラル型で文字列パターンを解析したいとき

inferを使わなくてよい場面:

  • 組み込みの ReturnType<T>Parameters<T> で済む場合は、自作せずそれらを使う
  • 単純にジェネリック型の型引数にアクセスするだけなら、型引数を直接渡すほうがシンプル
  • 型が複雑になりすぎてチームメンバーが読めない場合は、型の設計自体を見直す
// 組み込みで済むなら自作しない
type Good = ReturnType<typeof fetchUser>;

// わざわざinferで再実装する必要はない
type Redundant<T> = T extends (...args: any[]) => infer R ? R : never;

まとめ

TypeScriptの infer は、条件型と組み合わせて型の内部構造からパーツを抽出するキーワードです。TypeScript 2.8で導入されて以降、Variadic Tuple Types(4.0)、テンプレートリテラル型(4.1)、infer extends 制約(4.7)、リテラル型推論の改善(4.8)と段階的に強化されてきました。

基本パターンは「関数の戻り値型・引数型の抽出」「Promiseの展開」「配列要素型の取り出し」の3つを押さえれば、実務で必要な場面の大半をカバーできます。さらに infer extends を使った型制約や、テンプレートリテラル型での文字列パース、再帰型との組み合わせを習得すると、ライブラリレベルの高度な型定義も記述可能です。

バージョンリリース時期inferに関する主な変更
TypeScript 2.82018年3月条件型と infer キーワードの導入
TypeScript 4.02020年8月Variadic Tuple Typesでタプル操作が柔軟に
TypeScript 4.12020年11月テンプレートリテラル型内での infer 活用
TypeScript 4.52021年11月組み込み Awaited<T> ユーティリティ型
TypeScript 4.72022年5月infer extends 制約の追加
TypeScript 4.82022年8月テンプレートリテラル内でのリテラル型推論改善

日常のTypeScript開発で infer が活躍する場面に遭遇したら、まず組み込みユーティリティ型で対応できないかを確認し、対応できない場合に自作の条件型 + infer を検討する、という流れが効率的です。