Back

TypeScriptにおける`infer`の理解

TypeScriptにおける`infer`の理解

inferは条件型のextends節の中で型変数を宣言し、マッチしたサブタイプをキャプチャしてtrue分岐で使用できるようにするキーワードです。この位置以外では意味を持たず、他の場所で使用するとコンパイルエラーになります。ライブラリの.d.tsファイルや同僚のPRでT extends (...args: any[]) => infer R ? R : anyのような記述に出会ったことがあるなら、その行はデストラクチャリングがオブジェクトから値を取り出すように、inferを使って型の一部を抽出しています。

重要なポイント

  • inferは条件型のextends節の中でのみ使用可能であり、導入された変数はtrue分岐のスコープ内にのみ存在します。false分岐で参照するとコンパイルエラーになります。
  • inferTypeScript 2.8で導入されました。テンプレートリテラルのinfer4.1で、Awaited<T>4.5で、infer X extends Constraint4.7で追加されました。
  • 型パラメータが裸の型変数であり、ユニオン型を渡した場合、条件型は各メンバーに対して個別に分配されます。そのためinferはメンバーごとに一度解決され、結果がユニオンとして収集されます。これはよくある「暗黙の型の拡大」バグの原因です。
  • TypeScript 4.5以降、Awaited<T>がPromiseチェーンをアンラップするための組み込み型として提供されています。現代のTypeScriptでUnwrapPromiseを手動で実装する理由はありません。
  • 同じinfer変数に対して複数の候補がある場合、共変位置ではユニオン型が、反変位置では交差型が生成されます。

条件型:inferが存在する基盤

条件型は、ある型が別の型に代入可能かどうかに基づいて2つの型のいずれかを選択し、T extends X ? A : Bという形式を使います。型レベルの三項演算子のように機能します。TXに代入可能であれば型はAに解決され、そうでなければBに解決されます。TypeScriptハンドブックの条件型のページが主要なリファレンスです。

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

type A = IsString<"hello">; // true
type B = IsString<42>;      // false

多くの人が混乱するのは分配性の挙動です。チェックされる型が裸の型パラメータ(ラップされていないTそのもの)であり、ユニオン型を渡した場合、TypeScriptは条件型を各ユニオンメンバーに対して個別に分配し、結果を新しいユニオンとして収集します。タプルでパラメータをラップする([T] extends [X])と分配が無効になり、ユニオン全体を単一のユニットとしてチェックします。この違いは分配的条件型のドキュメントに記載されています。

type Distributed<T> = T extends string ? T[] : never;
type  R1  =  Distributed<string | number>; // string[] (numberメンバーはneverに解決され除外される)

type NonDistributed<T> = [T] extends [string] ? T[] : never;
type R2 = NonDistributed<string | number>; // never — 単一のユニットとしてチェックされる

このルールを頭に入れておいてください。後述する注意点のセクションで取り上げる、最も一般的なinferの落とし穴の根本原因です。

inferパターンの構造

inferで導入された型変数は条件型のtrue分岐のスコープ内にのみ存在します。false分岐で参照するとTypeScriptコンパイラエラーになります。 inferextendsパターンの中で変数を導入し、条件が評価される際にその位置にマッチしたものにバインドします。これによりinferはデストラクチャリングに似た動作をしますが、型に対して機能します。期待する形状を反映したパターンを記述し、抽出したい部分に名前を付けると、TypeScriptがそれを埋めてくれます。

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

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

パターン(infer U)[]は「何らかの要素型の配列 — その要素型をUと呼ぶ」という意味です。Tstring[]の場合、Ustringにバインドされます。マッチに失敗した場合はfalse分岐が実行され、Uはそこでは利用できません。

type Bad<T> = T extends (infer U)[] ? U[] : U;
//                                          ^ Error: Cannot find name 'U'.

1つのパターンに複数のinfer変数を含めることができ、それぞれが独自の位置にバインドされます。

type SplitFn<T> = T extends (arg: infer A) => infer R ? [A, R] : never;

type S = SplitFn<(x: number) => string>; // [number, string]

ここでinfer Aはパラメータ型をキャプチャし、infer Rは1回のパスで戻り値の型をキャプチャします。これはまさに組み込みユーティリティ型が使用しているテクニックです。

標準ライブラリにおけるinfer:定番の抽出パターン

TypeScript標準ライブラリはinferを使って抽出ユーティリティを実装しています。それぞれ同じ形状に従っています。パターンにマッチし、対象の位置をバインドし、true分岐でそれを返します。以下の定義はlib/es5.d.tsに含まれているものです。

関数の戻り値の型 — ReturnType

ReturnType<T>は関数の形状にマッチし、戻り値の位置をinfer Rにバインドすることで関数型の戻り値の型を抽出します。標準ライブラリの定義ではTが呼び出し可能であることを制約しています。

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

type A = ReturnType<() => string>;        // string
type B = ReturnType<() => { id: number }>; // { id: number }

ジェネリクス制約T extends (...args: any) => anyは実際の標準ライブラリ定義の一部であり、オプションの装飾ではないことに注意してください。

まとめ: ReturnTypeinferの最もわかりやすい実例です。関数をパターンマッチし、戻り値のスロットをキャプチャします。

タプルとしての関数パラメータ — Parameters

Parameters<T>はrest-parameterの位置をinfer Pにバインドすることで、関数のパラメータリストをタプルとして抽出します。

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

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

まとめ: パラメータはタプル型としてモデル化されているため、...argsに対するinfer Pはラベルとオプション性を保持しながらリスト全体を取得できます。

Promiseの解決 — 手書きのアンラッパーではなくAwaited<T>を使用する

TypeScript 4.5以降、組み込みのAwaited<T>Promiseチェーンを再帰的にアンラップし、PromiseLikeも処理します。現代のTypeScriptでUnwrapPromiseを手動で実装する理由はありません。多くの古いチュートリアルでは独自のT extends Promise<infer U> ? U : Tを定義していますが、この単一レベルのバージョンはネストされたPromiseをアンラップせず、.then可能なオブジェクトもモデル化しません。Awaitedはその両方を処理します。詳細はTypeScript 4.5のリリースノートに記載されています。

type A = Awaited<Promise<string>>;            // string
type B = Awaited<Promise<Promise<number>>>;   // number — 再帰的
type C = Awaited<boolean | Promise<number>>;  // number | boolean

まとめ: Awaited<T>を直接使用してください。inferベースのPromiseアンラッパーを自分で書くのは学習目的のみにしてください。

配列の要素型

配列の要素型を抽出するのは最もシンプルで実用的なinferパターンです。(infer U)[]にマッチしてUを返します。

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

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

まとめ: パターンにreadonlyを含めることで、ミュータブルな配列とreadonly配列の両方でユーティリティが機能します。

タプルのhead、tail、rest

タプルはrestエレメントを使った位置的なinferパターンをサポートしており、先頭、末尾以降、または最後の要素を取り出すことができます。パターンはJavaScriptの配列デストラクチャリングを反映しています。

type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type Tail<T extends any[]> = T extends [any, ...infer Rest] ? Rest : never;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;

type H = Head<[1, 2, 3]>; // 1
type R = Tail<[1, 2, 3]>; // [2, 3]
type L = Last<[1, 2, 3]>; // 3

まとめ: [infer Head, ...infer Rest][...any[], infer Last]は再帰的なタプル操作の基本要素です。型レベルのルーターやフォーム状態ライブラリでも同じパターンが活用されています。

再帰的な推論 — Flatten

inferと再帰を組み合わせることで、任意の深さの構造を展開する型を作れます。Flattenは非配列型に到達するまでネストされた配列を再帰的に展開します。

type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;

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

各パスでUが要素型にバインドされ、Flattenに再度フィードされます。Tが配列でなくなると、false分岐がそのまま返します。

まとめ: 再帰的なinferはネストされたコンテナを展開する方法です。深くネストされたPromiseやオブジェクト型のアンラップにも同じ形状が使えます。

テンプレートリテラル推論 — 型レベルでの文字列パース

テンプレートリテラル型は置換位置にinferをサポートしています。以下のExtractId<T>の例では文字列パターンにマッチし、可変部分をIdにバインドすることで、ランタイムパーサーなしに型付きのルートパラメータ抽出が可能になります。このパターンはTypeScript 4.1以降で利用できます。

type ExtractId<T> = T extends `/users/${infer Id}` ? Id : never;

type A = ExtractId<"/users/42">;   // "42"
type B = ExtractId<"/posts/42">;   // never

複数のセグメントにわたってinferを連鎖させることで、パス全体をその構成要素にパースできます。

type RouteParams<T> =
  T extends `${infer _Start}/:${infer Param}/${infer Rest}`
    ? Param | RouteParams<`/${Rest}`>
    : T extends `${infer _Start}/:${infer Param}`
      ? Param
      : never;

type P = RouteParams<"/users/:userId/posts/:postId">; // "userId" | "postId"

これは型レベルのパスパーサーに相当し、ルート文字列からパラメータオブジェクトを導出するフレームワークやライブラリにおける型付きルーティングの基盤となっています。

まとめ: テンプレートリテラルのinferは文字列形状の型をランタイムコストゼロで構造化データに変換します。ルートパラメータ、CSSプロパティのユニオン、ブランド文字列フォーマットのパースに役立ちます。

制約付き推論:infer X extends Constraint(TS 4.7)

TypeScript 4.7ではinfer X extends Constraintが追加され、条件型がマッチング時点で推論された変数を制約できるようになりました。推論と制約チェックを1つのステップで組み合わせられます。詳細はTypeScript 4.7のリリースノートに記載されています。4.7以前は、まず推論してから2つ目のネストされた条件型で結果を絞り込む必要がありました。

// 4.7以前: 推論してから2つ目の条件型でチェック
type FirstStringOld<T> =
  T extends [infer H, ...any[]]
    ? H extends string ? H : never
    : never;

// 4.7以降: 推論時点で制約を付与
type FirstString<T> =
  T extends [infer H extends string, ...any[]] ? H : never;

type A = FirstString<["hi", 1, 2]>; // "hi"
type B = FirstString<[1, 2, 3]>;    // never

TypeScript 4.8では、numberbigintbooleanなどのプリミティブ型に対する制約付き推論がさらに改善され、テンプレート文字列パターンのマッチング時により精密なリテラル型をコンパイラが保持できるようになりました。これはTypeScript 4.7で導入された制約付きinfer構文を基盤としていますが、別の機能強化として提供されました。

まとめ: キャプチャした型とその形状の保証の両方を1つの式で得たい場合はinfer X extends Cを使用してください。従来の「推論してからネスト」パターンを置き換えられます。

推論を暗黙的に壊す3つの落とし穴

inferを暗黙的に壊す3つの落とし穴は、ユニオン型に対する分配性による結果の拡大、複数のinfer候補における変性によるユニオンと交差型の切り替え、そして過度に具体的なパターンがfalse分岐に落ちることです。inferの失敗は通常サイレントです。型はコンパイルされますが、意図より広いまたは狭い型になっています。

1. 分配性が暗黙的に結果を拡大する

型パラメータTが裸の型変数であり、ユニオン型を渡した場合、TypeScriptは各メンバーに対して個別に条件型を分配します。つまりinfer Rはユニオンメンバーごとに1回解決され、結果がユニオンとして収集されるため、意図した以上に推論された型が暗黙的に拡大される可能性があります。

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

// 1つの型を返すように見えるが、実際には分配される
type R = Unwrap<Promise<string> | Promise<number>>; // string | number

混合ユニオンを展開するのではなく拒否したい場合は、パラメータをラップして分配を無効にしてください。

type UnwrapStrict<T> = [T] extends [Promise<infer U>] ? U : T;

まとめ: 裸のTとユニオン型の組み合わせはメンバーごとの評価を意味します。ユニオンを単一のユニットとして扱いたい場合は[T]でラップしてください。

2. 共変位置と反変位置で結果が変わる

同じinfer変数に対して複数の候補がある場合、共変位置(戻り値の型など)ではユニオン型が、反変位置(関数パラメータ型など)では交差型が生成されます。オーバーロードされた関数の場合、推論は最後のシグネチャを使用します。この動作はTypeScript 2.8のリリースノートに記載されています。

// 共変(戻り値の位置): ユニオン
type Co<T> = T extends { a: () => infer U; b: () => infer U } ? U : never;
type C = Co<{ a: () => string; b: () => number }>; // string | number

// 反変(パラメータの位置): 交差型
type Contra<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never;
type D = Contra<{ a: (x: string) => void; b: (x: number) => void }>; // string & number

まとめ: 複数の位置で同じinfer名を再利用するのは意図的で強力なテクニックですが、ユニオンになるか交差型になるかは位置の変性によって決まります。ユニオンになると思い込まないでください。

3. バインドできないパターンはfalse分岐に落ちる

inferは候補の型がパターンに構造的にマッチした場合にのみバインドされます。形状がマッチしない場合、条件はfalse分岐を取り、推論された変数は解決されません。そのため、過度に具体的なパターンは期待した値の代わりにフォールバック型を暗黙的に返します。

type FirstArg<T> = T extends (a: infer A, b: infer B) => any ? A : never;

type X = FirstArg<string>; // never — stringは関数型にマッチしない

この例では、FirstArgは関数型を期待しています。stringを渡してもそのパターンにマッチしないため、条件はfalse分岐に解決されneverを返します。

まとめ: パターンはマッチが許す限り緩くしてください。特定のアリティを拒否したい場合を除き、固定アリティの形状ではなく...args: infer P[infer H, ...any[]]を使用してください。

実際の現場でinferに出会う場面

アプリケーションコードではinferを自分で書くことはほとんどありません。ReactのComponentProps、ExpressのRequestHandler、Redux ToolkitのPayloadAction、RTK Queryのエンドポイント型などのライブラリ型定義の中で出会い、その結果のユーティリティ型を使用します。T extends Wrapper<infer X> ? X : Fallbackというパターンを認識できれば、それらの定義をブラックボックスとして扱うのではなく、読み解けるようになります。

ReactのComponentProps。 @types/reactの型定義では、inferを使ってComponentPropsを定義し、ホスト要素とコンポーネントコンストラクタの間で分岐しながらコンポーネントのpropsを型から取り出しています。読んでみると、ReturnTypeと同じT extends ... infer P ... ? P : ...の形状がReactの要素型に適用されているのがわかります。

Expressのリクエストハンドラー。 Expressの@types/expressRequestHandlerをルートパラメータ、レスポンスボディ、リクエストボディ、クエリのパラメータを持つジェネリクスとしてモデル化しています。パス文字列から型付きルートハンドラーを導出するライブラリは、これらのジェネリクスとテンプレートリテラルのinferを組み合わせて:paramセグメントを抽出します。先ほど示したRouteParamsパターンがその仕組みの核心です。

Redux ToolkitのPayloadAction。 Redux ToolkitのPayloadActioninferで抽出するのに便利な型ですが、その定義自体は内部的にinferではなくジェネリクスと条件型に依存しています。これはinferがライブラリの書き方ではなく、ライブラリの型を読むための手段であることを示す好例です。リデューサーのアクションからペイロード型を取り出すためにtype Payload<A> = A extends PayloadAction<infer P> ? P : neverを書くのは一般的なパターンです。

import type { PayloadAction } from "@reduxjs/toolkit";

type PayloadOf<A> = A extends PayloadAction<infer P> ? P : never;

type P = PayloadOf<PayloadAction<{ id: string }>>; // { id: string }

RTK Queryのエンドポイント。 RTK Query(Redux Toolkitの一部)は、クエリと結果の型がinferベースのユーティリティで取り出せる生成済みエンドポイント型を公開しています。結果の位置をバインドする条件型でエンドポイントの結果型を抽出するのは、パターンを認識してしまえば日常的な作業です。

まとめ

inferはルールが1つだけの機能です。条件型パターン内の位置に名前を付け、そこにマッチしたものをバインドし、true分岐でのみ使用できます。T extends Pattern<infer X> ? X : Fallbackを型レベルのデストラクチャリングとして見られるようになれば、すべての組み込みユーティリティとライブラリの型定義がその1つの動作のバリエーションとして読めるようになります。次に.d.tsファイルでinferに出会ったときは、パターンを追ってみてください。変数が位置するポジションを見つけ、パラメータが裸かラップされているかを確認し、true分岐を読んでいることを確かめてください。まずコードベースにある手書きのUnwrapPromiseAwaited<T>に置き換えることから始め、結果を絞り込むために2つ目の条件型をネストしている箇所ではinfer X extends Constraintを採用してみてください。

よくある質問

inferは条件型の中でパターンマッチングを使って既存の型からサブタイプを抽出します。一方、マップ型は型の各プロパティを新しい形状に変換します。関数の戻り値の型や配列の要素型など、構造からサブタイプを読み取る必要がある場合はinferを使用してください。キーを反復して新しいオブジェクト型を生成する必要がある場合はマップ型を使用してください。両者は逆の問題を解決します。inferは読み取り、マップ型は書き換えます。

いいえ。inferで導入された型変数は条件型のtrue分岐のスコープ内にのみ存在します。false分岐で参照すると「Cannot find name」などのTypeScriptコンパイラエラーが発生します。変数はマッチが成功した場合にのみ存在するため、false分岐にはバインドする値がありません。両方の結果で値が必要な場合は、true分岐でinferを使用し、false分岐には無関係なフォールバック型を指定してください。

分配性が原因です。型パラメータが裸の型変数であり、ユニオン型を渡した場合、TypeScriptは各メンバーに対して個別に条件型を分配します。そのためinferはユニオンメンバーごとに1回解決され、結果が新しいユニオンとして収集されます。これにより意図した以上に結果が暗黙的に拡大される可能性があります。分配を無効にしてユニオンを単一のユニットとしてチェックするには、[T] extends [Promise<infer U>] ? U : Tのようにパラメータをタプルでラップしてください。

いいえ、現代のTypeScriptでは不要です。TypeScript 4.5以降、組み込みのAwaited<T>がPromiseチェーンを再帰的にアンラップし、PromiseLikeのthenableオブジェクトも処理します。手書きのT extends Promise<infer U> ? U : Tは1レベルしかアンラップせず、thenableオブジェクトも無視するため、ネストされたPromiseでは正しく動作しません。アプリケーションコードではAwaited<T>を直接使用し、inferベースのアンラッパーを自分で書くのは学習目的のみにしてください。

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay