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;

読み方は次のとおりです。

  1. null と undefined はそのまま返す
  2. then メソッドを持つオブジェクトなら、then の第1引数(コールバック)の型を F として取り出す
  3. F の第1引数の型 V が「解決された値」なので、V に対してもう一度同じ処理をする(再帰)
  4. 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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?