- TypeScript
TypeScript inferの使い方|ReturnType自作と型の取り出し
infer とは、条件型の extends の右側に書いた「型の形」の一部に名前を付け、その部分の型を取り出すキーワードです。 T extends (...args: any) => infer R ? R : never と書くと、T が関数のときに戻り値の型が R に入ります。分割代入の型バージョン、と考えると読みやすくなります。
この記事は infer のパターン集です。関数・クラス・Promise・配列・タプル・オブジェクト・文字列のそれぞれから型を取り出す書き方を、コピーして使える形で並べ、すべて tsc で結果の型を確かめています。
確認環境:Ubuntu 24.04 / Node.js 22.22.2 / TypeScript 7.0.2(npx tsc --version の表示は Version 7.0.2)。エラー文はすべて npx tsc --noEmit --strict の出力です。
何から何を取り出すかの早見表
| 取り出したいもの | パターン(extends の右側) | 標準の型 |
|---|---|---|
| 関数の戻り値 | (...args: any) => infer R |
ReturnType |
| 関数の引数すべて(タプル) | (...args: infer P) => any |
Parameters |
| 関数の第1引数 | (first: infer A, ...rest: any[]) => any |
なし |
| 関数の this の型 | (this: infer S, ...args: any[]) => any |
ThisParameterType |
| コンストラクタの引数 | abstract new (...args: infer P) => any |
ConstructorParameters |
| new したときのインスタンス | abstract new (...args: any) => infer I |
InstanceType |
| Promise の中身(1段) | Promise<infer V> |
なし |
| Promise の中身(何段でも) | then の引数から取り出して再帰 | Awaited |
| 配列の要素 | readonly (infer E)[] |
なし(T[number] でも可) |
| タプルの先頭 | readonly [infer H, ...unknown[]] |
なし |
| タプルの末尾 | readonly [...unknown[], infer L] |
なし |
| タプルの2番目以降 | readonly [unknown, ...infer R] |
なし |
| オブジェクトのプロパティ | { port: infer V } |
なし(T["port"] でも可) |
| オブジェクトの値すべて | { [key: string]: infer V } |
なし |
| Map の値・Set の要素 | Map<any, infer V>・Set<infer V> |
なし |
| 文字列の接頭辞より後ろ | `user_${infer Rest}` |
なし |
| 文字列の先頭1文字 | `${infer C}${string}` |
なし |
| 文字列を数値リテラル型に | `${infer N extends number}` |
なし |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript inferの使い方|ReturnType自作と型の取り出し」
infer の読み方:3つの決まり
パターン集に入る前に、どの例にも共通する決まりを押さえます。
| 決まり | 内容 |
|---|---|
| 書ける場所 | 条件型の extends の右側だけ |
| 使える場所 | 条件が成り立った側(? の直後)だけ |
| 名前 | R・P・E などは自由に付けられる。慣習で1文字が多い |
type MyReturnType<T> = T extends (...args: any) => infer R ? R : never;
// ~~~~~~~~~~~~~~~~~~~~~~~~ ~
// 形を書き、取り出す所に infer 取り出した型を使う
「T がこの形に当てはまるなら、infer を置いた部分を取り出す。当てはまらなければ never」 と読みます。
検証の方法:型が違えば TS2322 が出る Equal 型
型の結果は実行時には残らないので、「期待する型と一致していなければコンパイルが止まる」 仕組みで確かめます。この記事では次の Equal 型を使います。
// e1.ts
type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type MyReturnType<T> = T extends (...args: any) => infer R ? R : never;
function total(prices: number[]) {
return prices.reduce((a, b) => a + b, 0);
}
const ok: Equal<MyReturnType<typeof total>, number> = true;
const ng: Equal<MyReturnType<typeof total>, string> = true;
$ npx tsc --noEmit --strict e1.ts
e1.ts(11,7): error TS2322: Type 'true' is not assignable to type 'false'.
10行目(number)は通り、11行目(string)だけが止まりました。Equal の結果が false 型になり、そこへ true を代入しようとしたためです。--locale ja を付けると、同じエラーが日本語で表示されます。
e1.ts(11,7): error TS2322: 型 'true' を型 'false' に割り当てることはできません。
以降のコードはすべてこの方式で確かめ、npx tsc --noEmit --strict がエラーなしで終わったものを載せています。
コピペ用:取り出す型のパターン集
次のファイルは、早見表のパターンをすべて収めた完成形です。そのまま保存して tsc にかけられます。
// infer-ok.ts
type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
// ---- 関数から取り出す ----
type MyReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : never;
type MyParameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
type FirstArg<T> = T extends (first: infer A, ...rest: any[]) => any ? A : never;
type ThisOf<T> = T extends (this: infer S, ...args: any[]) => any ? S : never;
// ---- クラス(コンストラクタ)から取り出す ----
type MyConstructorParameters<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: infer P) => any ? P : never;
type MyInstanceType<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: any) => infer I ? I : never;
// ---- Promise の中身 ----
type Unwrap<T> = T extends Promise<infer V> ? V : T;
type MyAwaited<T> =
T extends null | undefined ? T :
T extends object & { then(onfulfilled: infer F, ...args: infer _): any } ?
F extends (value: infer V, ...args: infer _) => any ? MyAwaited<V> : never :
T;
// ---- 配列・タプル ----
type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
type Head<T> = T extends readonly [infer H, ...unknown[]] ? H : never;
type Last<T> = T extends readonly [...unknown[], infer L] ? L : never;
type Tail<T> = T extends readonly [unknown, ...infer R] ? R : never;
// ---- オブジェクト・ジェネリクス ----
type PropOf<T, K extends PropertyKey> = T extends { [P in K]: infer V } ? V : never;
type ValueOf<T> = T extends { [key: string]: infer V } ? V : never;
type MapValue<T> = T extends Map<any, infer V> ? V : never;
type SetItem<T> = T extends Set<infer V> ? V : never;
// ---- 文字列 ----
type AfterPrefix<S, P extends string> = S extends `${P}${infer Rest}` ? Rest : never;
type FirstChar<S> = S extends `${infer C}${string}` ? C : never;
type Ext<S> = S extends `${string}.${infer E}` ? E : never;
type ToNumber<S> = S extends `${infer N extends number}` ? N : never;
// ---- 検証用の材料 ----
function findUser(id: number, withPosts: boolean) {
return { id, name: "sato", posts: withPosts ? ["a"] : [] };
}
async function loadCount(): Promise<number> { return 3; }
function greet(this: { name: string }, msg: string) { return msg + this.name; }
class Item { constructor(public name: string, public price: number) {} }
type Config = { port: number; host: string };
// ---- 検証:型が違えば TS2322 が出る ----
const t01: Equal<MyReturnType<typeof findUser>, { id: number; name: string; posts: string[] }> = true;
const t02: Equal<MyReturnType<typeof findUser>, ReturnType<typeof findUser>> = true;
const t03: Equal<MyParameters<typeof findUser>, [id: number, withPosts: boolean]> = true;
const t04: Equal<MyParameters<typeof findUser>, Parameters<typeof findUser>> = true;
const t05: Equal<FirstArg<typeof findUser>, number> = true;
const t06: Equal<ThisOf<typeof greet>, { name: string }> = true;
const t07: Equal<MyConstructorParameters<typeof Item>, [name: string, price: number]> = true;
const t08: Equal<MyConstructorParameters<typeof Item>, ConstructorParameters<typeof Item>> = true;
const t09: Equal<MyInstanceType<typeof Item>, Item> = true;
const t10: Equal<Unwrap<Promise<string>>, string> = true;
const t11: Equal<Unwrap<Promise<Promise<string>>>, Promise<string>> = true;
const t12: Equal<MyAwaited<Promise<Promise<string>>>, string> = true;
const t13: Equal<MyAwaited<ReturnType<typeof loadCount>>, number> = true;
const t14: Equal<MyAwaited<string | Promise<number>>, Awaited<string | Promise<number>>> = true;
const t15: Equal<ElementOf<string[]>, string> = true;
const t16: Equal<ElementOf<readonly [1, "a"]>, 1 | "a"> = true;
const t17: Equal<Head<[string, number, boolean]>, string> = true;
const t18: Equal<Last<[string, number, boolean]>, boolean> = true;
const t19: Equal<Tail<[string, number, boolean]>, [number, boolean]> = true;
const t20: Equal<Head<[]>, never> = true;
const t21: Equal<PropOf<Config, "port">, number> = true;
const t22: Equal<ValueOf<Config>, string | number> = true;
const t23: Equal<MapValue<Map<string, Date>>, Date> = true;
const t24: Equal<SetItem<Set<boolean>>, boolean> = true;
const t25: Equal<AfterPrefix<"user_name", "user_">, "name"> = true;
const t26: Equal<FirstChar<"abc">, "a"> = true;
const t27: Equal<Ext<"report.final.pdf">, "final.pdf"> = true;
const t28: Equal<ToNumber<"42">, 42> = true;
const t29: Equal<ToNumber<"abc">, never> = true;
$ npx tsc --noEmit --strict infer-ok.ts
$ echo $?
0
29件すべてが通りました。以下、種類ごとに要点を見ていきます。
関数から取り出す:ReturnType と Parameters を自作する
戻り値を取り出す(ReturnType)
type MyReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : never;
戻り値の位置に infer R を置くだけ です。t02 のとおり、標準の ReturnType と同じ結果になります。型引数の制約 T extends (...args: any) => any は「関数以外を渡したらエラーにする」ためのもので、取り出し自体は右辺の条件型が行っています。
引数を取り出す(Parameters)
type MyParameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
残余引数 ...args の型の位置に infer P を置くと、引数全体がタプルで取れます。 findUser なら [id: number, withPosts: boolean] で、引数名もラベルとして残ります(t03)。
1つ目だけほしいときは、FirstArg のように先頭の引数だけに infer を置きます。MyParameters<F>[0] と書いても同じ結果です。
this の型を取り出す
type ThisOf<T> = T extends (this: infer S, ...args: any[]) => any ? S : never;
TypeScript では、関数の最初の引数を this: 型 と書いて this の型を宣言できます。その位置に infer を置けば取り出せます(t06)。
クラスから取り出す:ConstructorParameters と InstanceType を自作する
type MyConstructorParameters<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: infer P) => any ? P : never;
type MyInstanceType<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: any) => infer I ? I : never;
クラスの型は「new で呼べる関数」として new (...args) => インスタンス と書けます。 関数のパターンの先頭に new が付いただけです。abstract を付けておくと、抽象クラスも受け取れます。
渡すのは typeof Item です。Item と書くとインスタンスの型を指すため、コンストラクタの形に当てはまりません。
| 書き方 | 指すもの |
|---|---|
Item |
インスタンスの型(name と price を持つオブジェクト) |
typeof Item |
クラスそのものの型(new で呼べる) |
Promise の中身を取り出す:Awaited を自作する
1段だけほどく
type Unwrap<T> = T extends Promise<infer V> ? V : T;
Promise<string> から string が取れます(t10)。ただし Promise が二重のときは、外側の1段しかほどけません(t11)。
何段でもほどく(Awaited)
type MyAwaited<T> =
T extends null | undefined ? T :
T extends object & { then(onfulfilled: infer F, ...args: infer _): any } ?
F extends (value: infer V, ...args: infer _) => any ? MyAwaited<V> : never :
T;
読み方は次のとおりです。
- null と undefined はそのまま返す
- then メソッドを持つオブジェクトなら、then の第1引数(コールバック)の型を F として取り出す
- F の第1引数の型 V が「解決された値」なので、V に対してもう一度同じ処理をする(再帰)
- then を持たなければ、そのまま返す
Promise という名前ではなく then の形を見ている ので、then を持つ独自のオブジェクトにも使えます。t12〜t14 のとおり、二重の Promise もほどけ、標準の Awaited と同じ結果になりました。
infer _ は「形を合わせるために受けるが、使わない」部分に付けた名前です。
async 関数の結果の型がほしいときは、2つを組み合わせます。
type Result = Awaited<ReturnType<typeof loadCount>>; // number
配列・タプルから取り出す
type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
type Head<T> = T extends readonly [infer H, ...unknown[]] ? H : never;
type Last<T> = T extends readonly [...unknown[], infer L] ? L : never;
type Tail<T> = T extends readonly [unknown, ...infer R] ? R : never;
| 型 | 入力 | 結果 |
|---|---|---|
| ElementOf | string[] |
string |
| ElementOf | readonly [1, "a"] |
1 | "a" |
| Head | [string, number, boolean] |
string |
| Last | [string, number, boolean] |
boolean |
| Tail | [string, number, boolean] |
[number, boolean] |
| Head | [] |
never |
パターンに readonly を付けておくと、通常の配列と as const の配列の両方を受け取れます。 付けない場合の結果は、後半の「つまずきやすい箇所」で確かめます。
...infer R のように残余要素に infer を置くと、残り全体がタプルとして取れます。Tail を再帰で使えば、タプルを1つずつ処理する型が書けます。
オブジェクト・ジェネリクスから取り出す
type PropOf<T, K extends PropertyKey> = T extends { [P in K]: infer V } ? V : never;
type ValueOf<T> = T extends { [key: string]: infer V } ? V : never;
type MapValue<T> = T extends Map<any, infer V> ? V : never;
type SetItem<T> = T extends Set<infer V> ? V : never;
ジェネリクスの型引数の位置に infer を置けば、その型引数が取れます。 Map・Set に限らず、自分で定義した Box<T> のような型でも同じ書き方です。
プロパティ1つなら Config["port"]、値すべてなら Config[keyof Config] でも同じ型が得られます。infer を使う書き方は、「その形でなければ never にする」という分岐を同時に書きたいときに向いています。
文字列の一部を取り出す
テンプレートリテラル型の中に infer を置くと、文字列リテラル型の一部が取れます。
type AfterPrefix<S, P extends string> = S extends `${P}${infer Rest}` ? Rest : never;
type FirstChar<S> = S extends `${infer C}${string}` ? C : never;
type Ext<S> = S extends `${string}.${infer E}` ? E : never;
type ToNumber<S> = S extends `${infer N extends number}` ? N : never;
| 型 | 入力 | 結果 |
|---|---|---|
| AfterPrefix | "user_name" と "user_" |
"name" |
| FirstChar | "abc" |
"a" |
| Ext | "report.final.pdf" |
"final.pdf" |
| ToNumber | "42" |
42 |
| ToNumber | "abc" |
never |
Ext の結果が "pdf" ではなく "final.pdf" になっている点は、「つまずきやすい箇所」で扱います。
infer U extends string:取り出す型に制約を付ける
TypeScript 4.7 から、infer に extends で制約を付けられます。「この部分を取り出す。ただし string のときだけ」 と1行で書けます。
// e4.ts(Equal 型の定義は e1.ts と同じ)
// 制約なし:先頭が何であっても取り出す
type Head<T> = T extends [infer H, ...unknown[]] ? H : never;
// 制約あり:先頭が string のときだけ取り出す
type HeadIfString<T> = T extends [infer H extends string, ...unknown[]] ? H : never;
// 条件型を入れ子にする書き方(結果は同じ)
type HeadIfStringNested<T> =
T extends [infer H, ...unknown[]] ? (H extends string ? H : never) : never;
const a1: Equal<Head<[42, "x"]>, 42> = true;
const a2: Equal<HeadIfString<["id", 1]>, "id"> = true;
const a3: Equal<HeadIfString<[42, "x"]>, never> = true;
const a4: Equal<HeadIfStringNested<[42, "x"]>, never> = true;
a1〜a4 はすべて通りました。制約に合わないときは、条件が成り立たなかった側(never)に進みます。
文字列を数値・真偽値のリテラル型に変える
制約付きの infer をテンプレートリテラル型の中で使うと、文字列リテラル型を別の種類のリテラル型に変えられます。
type ToNumber<S> = S extends `${infer N extends number}` ? N : never;
type ToBoolean<S> = S extends `${infer B extends boolean}` ? B : never;
type ToBigInt<S> = S extends `${infer B extends bigint}` ? B : never;
const b1: Equal<ToNumber<"42">, 42> = true;
const b2: Equal<ToNumber<"3.14">, 3.14> = true;
const b3: Equal<ToNumber<"1e3">, number> = true;
const b4: Equal<ToNumber<"007">, number> = true;
const b5: Equal<ToNumber<"abc">, never> = true;
const b6: Equal<ToBoolean<"true">, true> = true;
const b7: Equal<ToBigInt<"10">, 10n> = true;
| 入力 | 結果 | 補足 |
|---|---|---|
"42" |
42 |
数値リテラル型になる |
"3.14" |
3.14 |
小数も可 |
"1e3" |
number |
リテラル型にはならず number になった |
"007" |
number |
同上 |
"abc" |
never |
数値として読めない |
制約がないと、string 用の型へ渡せない
取り出した型を Uppercase のような「string だけを受け取る型」に渡すとき、制約がないとエラーになります。
type Shout1<T> = T extends [infer H, ...unknown[]] ? Uppercase<H> : never;
type Shout2<T> = T extends [infer H extends string, ...unknown[]] ? Uppercase<H> : never;
e4.ts(33,64): error TS2344: Type 'H' does not satisfy the constraint 'string'.
Shout1 だけがエラーになり、infer H extends string と書いた Shout2 は通ります。 Shout2<["id", 1]> の結果は "ID" でした。
同じ名前の infer を複数箇所に置いたとき:ユニオンと交差型
同じ型変数を2か所以上に置くと、候補が複数になります。置いた位置によって、結果がユニオンになる場合と交差型になる場合があります。
| 置いた位置 | 結果 | 例 |
|---|---|---|
| 値を持つ・返す位置(プロパティ、戻り値、配列要素) | ユニオン | string | number |
| 関数の引数の位置 | 交差型 | { id: number } & { name: string } |
// e5.ts(Equal 型の定義は e1.ts と同じ)
// 値を「返す・持つ」位置に 2 回 → ユニオン
type Both<T> = T extends { a: infer U; b: infer U } ? U : never;
// 関数の「引数」の位置に 2 回 → 交差型
type BothArg<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never;
const u1: Equal<Both<{ a: string; b: string }>, string> = true;
const u2: Equal<Both<{ a: string; b: number }>, string | number> = true;
const i1: Equal<BothArg<{ a: (x: string) => void; b: (x: string) => void }>, string> = true;
const i2: Equal<
BothArg<{ a: (x: { id: number }) => void; b: (x: { name: string }) => void }>,
{ id: number } & { name: string }
> = true;
// プリミティブ同士の交差は never にまとまる
const i3: Equal<BothArg<{ a: (x: string) => void; b: (x: number) => void }>, never> = true;
すべて通りました。理由は「両方に当てはまる U は何か」を考えるとわかります。プロパティなら「string も number も入る型」=ユニオン、引数なら「どちらの関数にも渡せる値の型」=交差型です。string と number の交差型は該当する値がないため、never にまとまります(i3)。
応用:ユニオンを交差型に変える
引数の位置で交差型になる性質を使うと、ユニオンを交差型に変える型が書けます。
type UnionToIntersection<U> =
(U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
const i4: Equal<
UnionToIntersection<{ id: number } | { name: string }>,
{ id: number } & { name: string }
> = true;
前半でユニオンの各メンバーを「それを引数に取る関数」に変え、後半で引数の位置から1つの I として取り出しています。
同じ名前に違う制約は付けられない(TS2838)
// e7.ts
type SameName<T> = T extends [infer U extends string, infer U extends number] ? U : never;
e7.ts(1,37): error TS2838: All declarations of 'U' must have identical constraints.
e7.ts(1,61): error TS2838: All declarations of 'U' must have identical constraints.
同じ名前を複数箇所に置くときは、制約をそろえるか、名前を分けます。
エラーになる書き方と、エラー全文
条件型の extends の外に infer を書いた(TS1338)
// e2.ts
type Bad1 = infer U;
type Bad2<T extends (infer U)[]> = U;
type Bad3<T> = T extends string ? infer U : never;
e2.ts(1,13): error TS1338: 'infer' declarations are only permitted in the 'extends' clause of a conditional type.
e2.ts(2,22): error TS1338: 'infer' declarations are only permitted in the 'extends' clause of a conditional type.
e2.ts(2,36): error TS2304: Cannot find name 'U'.
e2.ts(3,35): error TS1338: 'infer' declarations are only permitted in the 'extends' clause of a conditional type.
--locale ja での表示は次のとおりです。
e2.ts(1,13): error TS1338: 'infer' 宣言は、条件付き型の 'extends' 句でのみ許可されます。
3つとも同じ理由です。
| 行 | 書いた場所 | 直し方 |
|---|---|---|
| Bad1 | 条件型ではない | T extends X<infer U> ? U : never の形にする |
| Bad2 | 型引数の制約(こちらも extends だが条件型ではない) | 制約は T extends unknown[] にして、右辺の条件型で infer する |
| Bad3 | 条件型の結果の側 | infer は ? より前、extends の右側に書く |
Bad2 を直すと次のようになります。
type Good2<T extends unknown[]> = T extends (infer U)[] ? U : never;
成り立たなかった側で型変数を使った(TS2304)
// e3.ts
type ElementOrSelf<T> = T extends (infer E)[] ? E : E;
e3.ts(1,53): error TS2304: Cannot find name 'E'.
infer で付けた名前が使えるのは、条件が成り立った側だけです。 形に当てはまらなかったときは取り出すものがないので、T extends (infer E)[] ? E : T のように元の T などを返します。
エラー番号の早見表
| 番号 | メッセージ(要約) | 原因 | 直し方 |
|---|---|---|---|
| TS1338 | infer は条件型の extends 句だけ | 条件型の外、または結果の側に書いた | extends の右側へ移す |
| TS2304 | Cannot find name | 成り立たなかった側で使った | 成り立った側だけで使う |
| TS2344 | does not satisfy the constraint | 取り出した型を制約のある型へ渡した | infer U extends string と制約を付ける |
| TS2838 | must have identical constraints | 同じ名前に違う制約 | 制約をそろえるか名前を分ける |
| TS2322 | 'true' is not assignable to 'false' | Equal 型での検証が不一致 | 期待する型か定義を見直す |
つまずきやすい箇所:思った型にならないとき
エラーは出ないものの、結果が never などになって気づきにくい例を集めました。次のコードは、コメントに書いた結果になることを tsc で確かめています(Equal 型の定義は e1.ts と同じ)。
// e6.ts
// 1) 引数を unknown[] で受けると、引数のある関数にマッチしない
type RetUnknown<T> = T extends (...args: unknown[]) => infer R ? R : never;
type RetAny<T> = T extends (...args: any[]) => infer R ? R : never;
type RetNever<T> = T extends (...args: never[]) => infer R ? R : never;
declare function byId(id: number): string;
const p1: Equal<RetUnknown<typeof byId>, never> = true;
const p2: Equal<RetAny<typeof byId>, string> = true;
const p3: Equal<RetNever<typeof byId>, string> = true;
// 2) オーバーロードは最後のシグネチャから取り出される
declare function conv(x: string): number;
declare function conv(x: number): string;
declare function conv(x: string | number): string | number;
const p4: Equal<RetAny<typeof conv>, string | number> = true;
// 3) ユニオンを渡すと、メンバーごとに取り出してユニオンに戻る
type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
const p5: Equal<ElementOf<string[] | number[]>, string | number> = true;
// 4) タプル用のパターンは、長さの決まっていない配列にマッチしない
type Head<T> = T extends readonly [infer H, ...unknown[]] ? H : never;
const p6: Equal<Head<string[]>, never> = true;
const p7: Equal<Head<readonly ["a", "b"]>, "a"> = true;
// 5) readonly を付けないパターンは、as const のタプルにマッチしない
type HeadMutable<T> = T extends [infer H, ...unknown[]] ? H : never;
const p8: Equal<HeadMutable<readonly ["a", "b"]>, never> = true;
// 6) 文字列の infer は、先頭側が最短で切られる
type Ext<S> = S extends `${string}.${infer E}` ? E : never;
type LastExt<S> = S extends `${string}.${infer E}` ? LastExt<E> : S;
const p9: Equal<Ext<"report.final.pdf">, "final.pdf"> = true;
const p10: Equal<LastExt<"report.final.pdf">, "pdf"> = true;
// 7) async 関数の戻り値は Promise のまま取れる
async function load() { return { id: 1 }; }
const p11: Equal<RetAny<typeof load>, Promise<{ id: number }>> = true;
const p12: Equal<Awaited<RetAny<typeof load>>, { id: number }> = true;
// 8) any を渡すと両方の枝のユニオンになる
type IsArray<T> = T extends (infer E)[] ? E : "no";
const p13: Equal<IsArray<any>, unknown> = true;
| 番号 | 起きること | こう書くとよい |
|---|---|---|
| 1 | 引数を unknown[] で受けると never |
引数は any[](または never[])で受ける |
| 2 | オーバーロードは最後のシグネチャが使われる | 最後に一番広いシグネチャを置く |
| 3 | ユニオンはメンバーごとに処理される | そのまま使える。止めたいときは [T] extends [...] と包む |
| 4 | string[] に先頭要素のパターンは当てはまらない |
配列は ElementOf、タプルは Head と使い分ける |
| 5 | readonly なタプルが never になる | パターンに readonly を付ける |
| 6 | 最初の区切りで切られる | 最後の区切りがほしいときは再帰する |
| 7 | Promise に包まれたまま | Awaited を重ねる |
| 8 | any は成り立つ側と成り立たない側の両方に進む | 入力が any にならないよう、手前で型を付ける |
2 のオーバーロードについては、公式ハンドブックの Conditional Types の節に「複数の呼び出しシグネチャを持つ型から推論するときは、最後のシグネチャから推論される」と書かれています。
応用:関数をまとめたオブジェクトから引数と結果の型を取り出す
ここまでのパターンを組み合わせた、動かせる完成形です。関数の定義から型を取り出しているので、関数の引数や戻り値を変えると、呼び出し側の型も自動で追従します。
// app.mts
// 関数をまとめたオブジェクトから、引数と結果の型を取り出す
const api = {
async getTask(id: number) {
return { id, title: "資料を作る", done: false };
},
async listTasks(page: number, perPage: number) {
return [{ id: 1, title: "資料を作る", done: false }];
},
};
type Api = typeof api;
type ArgsOf<K extends keyof Api> = Api[K] extends (...args: infer P) => any ? P : never;
type ResultOf<K extends keyof Api> =
Api[K] extends (...args: any) => Promise<infer R> ? R : never;
// 取り出した型で、呼び出しを記録する関数を作る
async function callWithLog<K extends keyof Api>(name: K, ...args: ArgsOf<K>): Promise<ResultOf<K>> {
console.log("call:", name, JSON.stringify(args));
const fn = api[name] as unknown as (...a: ArgsOf<K>) => Promise<ResultOf<K>>;
return fn(...args);
}
const task = await callWithLog("getTask", 7);
console.log(task.title, task.done);
const list = await callWithLog("listTasks", 1, 20);
console.log(list.length);
$ npx tsc --noEmit --strict --target es2022 --module nodenext app.mts
$ node app.mts
call: getTask [7]
資料を作る false
call: listTasks [1,20]
1
Node.js 22.22.2 は、型注釈だけの TypeScript ファイルをそのまま実行できます。
ResultOf では、戻り値の位置に Promise<infer R> と書いて「戻り値を取り出す」と「Promise をほどく」を1つのパターンで済ませています。
末尾に、引数の型が違う呼び出しと、つづりの違うプロパティを足して tsc にかけると、取り出した型が効いていることがわかります。
await callWithLog("getTask", "7");
console.log(task.titel);
app-ng.mts(29,30): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
app-ng.mts(30,18): error TS2551: Property 'titel' does not exist on type '{ id: number; title: string; done: boolean; }'. Did you mean 'title'?
標準の型で足りるか、自作するかの判断表
| やりたいこと | 選ぶもの |
|---|---|
| 関数の戻り値・引数 | 標準の ReturnType・Parameters |
| クラスの引数・インスタンス | 標準の ConstructorParameters・InstanceType |
| Promise の中身 | 標準の Awaited |
| 配列の要素・オブジェクトのプロパティ | T[number]・T["key"](添字でのアクセス) |
| 第1引数だけ、タプルの末尾、2番目以降 | infer で自作 |
自作ジェネリクス(Box<T> など)の型引数 |
infer で自作 |
| 文字列リテラル型の一部 | infer で自作 |
| 形に当てはまらないときは別の型にしたい | infer で自作 |
まず標準の型と添字でのアクセスを探し、それで届かない部分だけ infer で書く と、読む人にも伝わりやすい型になります。
よくある質問
Q. infer の後ろの R や U は決まった名前ですか?
A. 自由に付けられる名前です。Return の R、Parameters の P、Element の E のように頭文字にする慣習があります。長い名前(infer Return など)でも動きます。
Q. infer はジェネリクスの型引数と何が違いますか?
A. 型引数は使う側が渡すもの、infer は TypeScript が形から読み取って決めるものです。MyReturnType<typeof f> と書く側は R を渡しておらず、f の型から R が決まります。
Q. 形に当てはまらなかったとき、never 以外を返してもよいですか?
A. はい。T extends Promise<infer V> ? V : T のように元の型をそのまま返す書き方もよく使います。「当てはまらないものは対象外」としたいなら never、「当てはまらないものはそのまま通す」なら T を返します。
Q. infer を1つの条件型に複数書けますか?
A. 書けます。T extends (first: infer A, ...rest: infer R) => infer Ret ? [A, R, Ret] : never のように、違う名前を付ければそれぞれ別の部分を取り出せます。同じ名前を複数置いたときの結果は「ユニオンと交差型」の節のとおりです。
まとめ
- infer は条件型の extends の右側に書き、形の一部に名前を付けて取り出す。 使えるのは条件が成り立った側だけ
- 戻り値は
=> infer R、引数は...args: infer P、コンストラクタは先頭に new を付ける。 ReturnType・Parameters・ConstructorParameters はこの形で自作できる - Awaited は then の引数から取り出して再帰する。 1段だけなら
Promise<infer V>で足りる - タプルは
[infer H, ...unknown[]]、文字列はテンプレートリテラル型の中に infer を置く。 パターンには readonly を付けておくと as const の配列も受け取れる infer U extends stringで制約を付けると、入れ子の条件型が1行になる。 文字列を数値リテラル型に変える用途にも使える- 同じ名前を複数置くと、値の位置ならユニオン、引数の位置なら交差型になる
- 条件型の外に書くと TS1338、成り立たなかった側で使うと TS2304。 結果が never のときは、引数の
unknown[]と readonly の有無を確かめる
参考
- TypeScript Handbook「Conditional Types」(Inferring Within Conditional Types): https://www.typescriptlang.org/docs/handbook/2/conditional-types.html
- TypeScript 4.7 リリースノート(extends Constraints on infer Type Variables): https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-7.html
- TypeScript 2.8 リリースノート(Type inference in conditional types): https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「TypeScript inferの使い方|ReturnType自作と型の取り出し」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。