TypeScriptのinferとは|型抽出の書き方を基礎から完全理解
TypeScriptのコードを読んでいて、infer が出てきたとたんに型の意味が追いにくくなる、と感じたことはありませんか。
T extends (infer U)[] ? U : never みたいな型、何をやってるのか急に読めなくなる…
ReturnType の中身がinferらしいけど、自分で書けと言われると無理だ。
infer は、条件付き型の中で「型をその場で取り出す」ためのキーワードです。仕組みさえ一度つかめば、ReturnType のような標準ユーティリティの中身がスラスラ読めるようになり、自分でも型を抽出するユーティリティが書けるようになります。この記事では、infer が読めるようになり、最終的には自分の手で ReturnType 相当を書けるところまで到達します。
この記事は次のような方におすすめです。
- ジェネリクスやextendsは触ったが、inferが出ると型が読めなくなる人
- ReturnTypeなどの標準ユーティリティの中身を理解したい人
- 配列やPromiseから中身の型だけを取り出す書き方を知りたい人
- 自作の型ユーティリティを書けるようになりたい人
読み終えるころには、infer 入りの型を見ても怖くなくなり、つまずいたときに「何を疑えばいいか」まで判断できるようになります。
それでは、順を追って詳しく見ていきましょう!
- 未経験で後悔したくない
【実体験】未経験からITエンジニアに転職して後悔した話|4社経験してわかった「最初の選択ミス」 - 年収が低くて不安
4年間ずっと年収260万だったエンジニアが、転職で510万になるまでの全記録
inferとは何か|一言でいうと型を「その場で宣言」する仕組み
infer は、条件付き型の中で、まだ名前のない型を取り出して新しい型変数として宣言するキーワードです。「ここに当てはまる型に名前を付けて、後で使わせて」という指示だと考えると一気に読みやすくなります。
たとえば「配列の中身の型を取り出したい」とき、要素の型はその場では名前がありません。そこで infer U と書くと、TypeScriptが該当部分を推論して U という新しい型変数に束縛してくれます。
type ElementType<T> = T extends (infer U)[] ? U : never;
type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
ElementType<string[]> をエディタでホバーすると string と表示されます。(infer U)[] の U に、配列の要素である string が「その場で宣言」されて束縛された、というわけです。
重要なのは書ける場所です。infer は条件付き型の extends の右辺にしか書けません。それ以外の場所に書くとエラーになります。この位置の制約こそが、infer が読みにくく感じる最大の理由です。逆に言えば、infer を見たら「これは条件付き型の中だ」と分かるサインでもあります。
ジェネリクスと混同しやすいので、両者の違いは押さえておくとよいですよ。
inferの動きを図解で理解する
infer を書いた瞬間、型は「入力 → パターン照合 → 束縛 → 取り出し」という順に流れます。この流れを一度なぞると、複雑な型も同じパターンの組み合わせとして読めるようになります。
下の図は ElementType<string[]> を例に、string[] という入力がどう照合され、U に何が束縛され、最後に何が返るかを示したものです。
左から右へ、入力型が条件にマッチするかを判定し、マッチしたら該当部分が infer U の U に束縛され、true分岐(? U)でその U が返ります。infer で束縛した型変数が使えるのは、条件が成立したtrue分岐の中だけだと覚えておくと、後の罠も理解しやすくなります。
前提:Conditional Types(条件付き型)の基本
infer の土台は条件付き型(Conditional Types)です。構文は T extends U ? A : B で、読み方は「TがUに割り当て可能ならAを、そうでなければBを返す」となります。三項演算子の型版だと考えると直感的です。
type IsString<T> = T extends string ? "yes" : "no";
type R1 = IsString<string>; // "yes"
type R2 = IsString<number>; // "no"
IsString<string> は string が string に割り当て可能なので "yes"、IsString<number> は割り当て不可なので "no" になります。infer はこの extends の右辺(条件節)の中でしか使えません。つまり条件付き型を読めることが、infer を読む前提になります。
ここで使う extends は「割り当て可能かどうか」を判定しているだけで、クラスの継承や型引数の制約とは役割が異なります。
union型の分配がinferの抽出結果を変える
infer で型を取り出すとき、対象がunion型だと結果が想定とずれることがあります。その原因の多くがここで扱う「分配」です。条件付き型にUnion型を裸のまま渡すと、各メンバーごとに条件が適用されて結果が合成される挙動があり、これを分配的条件型(Distributive Conditional Types)と呼びます。仕組みを知っておくと、infer の抽出が思った型にならないときの原因切り分けに役立ちます。
type ToArray<T> = T extends any ? T[] : never;
type Distributed = ToArray<string | number>;
// string[] | number[] ← string・number それぞれに適用されて合成
string | number をまとめて1つの配列にするのではなく、string と number に別々に適用された結果が string[] | number[] になっています。infer と組み合わせるときも同様で、意図せず分配されると抽出結果が想定とずれることがあります。
分配が起きるのは、条件の左辺が裸の型パラメータ(T extends ...)のときだけです。[T] のようにタプルで包むと分配は抑止されます。
type NoDistribute<T> = [T] extends [any] ? T[] : never;
type NotDistributed = NoDistribute<string | number>;
// (string | number)[] ← まとめて1つの配列に
分配を止めたいときは [T] で包む、という対処をセットで覚えておくと安心です。
inferの実践パターン|型を抽出する4つの定番
infer の使いどころは「ある型の一部分を取り出す」ことに集約されます。代表的な抽出パターンは4つあり、いずれも extends のパターンに infer を差し込む形で書けます。
| 対象 | 書き方の骨子 | 取り出す型 | 標準ユーティリティ |
|---|---|---|---|
| 関数の戻り値 | T extends (...args: any) => infer R ? R : ... |
戻り値の型 | ReturnType<T> あり |
| 配列・タプルの要素 | T extends (infer U)[] ? U : ... |
要素の型 | なし(自作) |
| Promiseの解決値 | T extends Promise<infer V> ? V : ... |
解決値の型 | Awaited<T> あり |
| 関数の引数 | T extends (...args: infer P) => any ? P : ... |
引数のタプル | Parameters<T> あり |
なお、表の T extends Promise<infer V> ? V : ... は、Promiseの「外側1枚」だけを外す簡略版です。たとえば Promise<Promise<number>> のようにPromiseが二重にネストした型に当てると、外側の1枚しか外れず結果は Promise<number> のまま止まります。
type ResolvedType<T> = T extends Promise<infer V> ? V : T;
type One = ResolvedType<Promise<number>>; // number(1枚外れる)
type Two = ResolvedType<Promise<Promise<number>>>; // Promise<number>(外側1枚だけ)
// 標準の Awaited<T> は await / .then() と同じく、最後までネストをほどく
type Deep = Awaited<Promise<Promise<number>>>; // number
このように、標準の Awaited<T> は await や .then() の挙動に近く、Promiseを再帰的にアンラップするため Awaited<Promise<Promise<number>>> は最終的に number になります。深いネストまで確実に解決したいときは自作の1段版ではなく Awaited<T> を使う、と覚えておくと安全です。
それぞれの中身を実コードで分解していきます。
関数の戻り値の型を抽出する(ReturnType を分解)
標準の ReturnType は、まさに infer で戻り値を抜き出すユーティリティです。lib.es5.d.ts での定義を引用すると、次のようになっています。
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
出典:TypeScript 公式リポジトリsrc/lib/es5.d.ts(microsoft/TypeScript)
1行ずつ読むと次の通りです。
T extends (...args: any) => any:Tを「何らかの関数型」に制約しているT extends (...args: any) => infer R:その関数の戻り値の位置にinfer Rを置いて束縛? R : any:マッチすれば戻り値Rを、しなければanyを返す
これを自分で書くと、ReturnType とほぼ同じものが作れます。
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: 1, name: "Alice" };
}
type User = MyReturnType<typeof getUser>;
// { id: number; name: string }
MyReturnType<typeof getUser> をホバーすると { id: number; name: string } と表示されます。戻り値の位置に infer R を置くだけで、関数の返り値の型がそのまま取り出せるのがポイントです。
配列・タプルの要素型を抽出する
配列から中身の型だけを取り出すには、T extends (infer U)[] ? U : never と書きます。配列の形にパターンを合わせ、要素の位置に infer U を置くのが定石です。
type ElementType<T> = T extends (infer U)[] ? U : never;
type E1 = ElementType<number[]>; // number
type E2 = ElementType<{ id: number }[]>; // { id: number }
タプルの先頭要素だけを取り出したいときは、パターンを配列ではなくタプルの形にします。
type First<T> = T extends [infer H, ...any[]] ? H : never;
type F1 = First<[string, number, boolean]>; // string
[infer H, ...any[]] は「先頭が H、残りは何でもよい」というパターンで、先頭要素だけを H に束縛しています。
Promiseの解決値・関数の引数型を抽出する
ネストした型も、パターンを入れ子にすれば抜き出せます。Promiseの解決値を取り出すなら、Promise<infer V> と書きます。
type ResolvedType<T> = T extends Promise<infer V> ? V : T;
type P1 = ResolvedType<Promise<string>>; // string
type P2 = ResolvedType<number>; // number(Promiseでなければそのまま)
関数の引数の型をまとめて取り出すなら、引数リストの位置に infer P を置きます。複数の値を同時に infer することもできます。
type Params<T> = T extends (...args: infer P) => any ? P : never;
type Args = Params<(id: number, name: string) => void>;
// [id: number, name: string]
// 引数と戻り値を同時に取り出す
type Split<T> = T extends (...args: infer P) => infer R ? [P, R] : never;
type S = Split<(x: number) => boolean>;
// [[x: number], boolean]
Split では infer P と infer R を1つの条件付き型の中で同時に使い、引数と戻り値を一度に取り出して タプルにまとめています。
よくある質問
Q1:inferとジェネリクス(型引数)の違いは何ですか?
ジェネリクスは「外から型を受け取る入口」、infer は「条件付き型の中で型を取り出す抽出」です。<T> で受け取った T の内部から、infer でさらに一部分を抜き出す、という関係になります。役割が逆向きだと押さえると混同しません。型引数の基礎はジェネリクスの専用解説で確認できます。
Q2:inferは実務で本当に使いますか?自分で書く機会はある?
使います。ReturnType や Awaited など標準ユーティリティの中身が infer なので、ライブラリの型定義を読む場面では必ず遭遇します。自分で書く機会は、汎用の型ユーティリティを作るときに訪れます。読めるだけでも価値が大きく、書ければ表現の幅が広がります。
Q3:inferは条件付き型の中でどう使いますか?
infer は extends の右辺(条件付き型 T extends U ? A : B の U 側)でのみ書けます。そこにパターンとして infer R を置くと、該当部分が R に束縛され、true分岐で使えます。
【付録】さらに学びを深めるためのリソース
さらにTypescriptの学習を進めたい方のために、いくつかのリソースを紹介します。
これらのリソースを活用することで、TypeScriptの型システムについてより深い知識を得ることができるでしょう。
おすすめの書籍
ゼロからわかる TypeScript入門
技術評論社から出版されている「ゼロからわかる TypeScript入門」は、プログラミング初心者や本職プログラマーではない方を主な対象にした入門書です。
変数・条件分岐・ループといった基本から、クラスやインターフェース、モジュールまで段階的に学べる構成になっています。最終章ではWeb APIとJSONを使った非同期Webアプリの作成も体験できるので、「実際に動くものを作る」ところまで到達できます。
プロを目指す人のためのTypeScript入門
技術評論社の「プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで」、通称 ブルーベリー本 です。
JavaScriptの仕様とTypeScript独自の機能を両方押さえつつ、リテラル型・ユニオン型・keyof型・ジェネリクスなど、高度な型表現まで踏み込んで解説しています。TypeScriptの型システムの表現力を本格的に学べる一冊です。
オンラインで参照できる公式ドキュメント
TypeScript公式ハンドブック
https://www.typescriptlang.org/docs/
TypeScriptの公式ドキュメントです。
intersection型を含む、すべての型システムの機能について詳細な説明があります。
TypeScript Deep Dive
https://basarat.gitbook.io/typescript/
TypeScriptの深い部分まで掘り下げて解説しているオンラインブックです。
無料で読むことができ、intersection型についても詳しく説明されています。
TypeScriptの学習は終わりがありません。
新しい機能が常に追加され、より良い書き方が発見されています。
継続的に学習を続けることで、より良いTypeScriptプログラマーになれるはずです。
まとめ – inferは条件付き型で型をその場で抽出する仕組み
この記事のポイントをまとめます。
inferは条件付き型の中で、まだ名前のない型をその場で宣言・抽出する仕組み- 書ける場所は
extendsの右辺だけで、true分岐の中で束縛した型変数が使える ReturnType・配列要素・Promise解決値・関数引数は、いずれもパターンにinferを差し込んで抽出できる- つまずいたら「位置の制約」「同名inferの合成」「制約付きinfer」の3点を症状から切り分ける
仕組みを一度つかめば、infer 入りの型は「入力→照合→束縛→取り出し」のパターンとして読め、自分で ReturnType 相当のユーティリティも書けるようになります。
※本記事の本文案はAIを活用して作成していますが、記載している内容およびコードは筆者が実際に調査、検証・実行し、内容の正確性を確認した上で公開しています。






