1. ホーム
  2. TypeScript

【TypeScript】infer の使い方|Conditional Types で型の一部を抽出する

Share

TypeScript の型を書いていると、「ある型の中に隠れている一部分だけを取り出して使いたい」という場面がよくあります。たとえば配列 string[] から要素の型 string を、Promise<number> から中身の number を取り出す、といった具合です。こうした「型の一部を抜き出す」操作を担うのが infer キーワードです。この記事では、infer がどこで使えるのか、どんな仕組みでマッチした型を取り出すのかを、配列・Promise・関数などの具体例を追いながら丁寧に解説します。あわせて、複数の infer や制約付きの infer、そして初心者がつまずきやすいポイントも紹介します。

infer は Conditional Types の中で型を取り出すためのキーワード

infer は、Conditional Types(条件型)の中でだけ使える特別なキーワードです。Conditional Types とは T extends U ? X : Y という形の型で、「TU に代入できるなら X、そうでなければ Y」という三項演算子のような書き方をします。この extends の右側(条件の部分)に infer を書くと、「ここに当てはまる型を変数として捕まえておいて」という指示になります。

言い換えると、infer U は「TypeScript にパターンを照合させ、マッチした部分を型変数 U に束縛する」ための宣言です。正規表現のキャプチャグループに近いイメージで、型の構造の中から欲しい部分に infer の目印を付けておき、マッチが成功したらその部分を U として真の分岐で使えます。

配列の要素型を取り出す基本例

もっとも分かりやすい例として、配列型からその要素の型を取り出してみます。「T が『何らかの型の配列』なら、その要素の型を返す」という条件型は次のように書けます。

element-type.ts
// T が「何らかの型 U の配列」にマッチするなら U を、そうでなければ never を返す
type ElementType<T> = T extends (infer U)[] ? U : never;

type A = ElementType<string[]>;   // string
type B = ElementType<number[]>;   // number
type C = ElementType<boolean>;    // never(配列ではないのでマッチしない)

T extends (infer U)[] の部分がポイントです。(infer U)[] は「要素が U である配列」というパターンで、U がどんな型かはこの時点では決めていません。string[] を渡すと TypeScript が U = string だと推論し、真の分岐で U(= string)を返します。配列でない boolean を渡すとパターンにマッチしないため、偽の分岐の never になります。

Promise の中身の型を取り出す

同じ考え方で、Promise が解決する値の型も取り出せます。パターンを Promise<infer U> にするだけです。非同期関数の戻り値を扱うときなどに役立ちます。

unwrap-promise.ts
// T が Promise<U> なら中身の U を、そうでなければ T をそのまま返す
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 でないのでそのまま)

ここでは偽の分岐を never ではなく T にしています。こうすると「Promise ならほどく、そうでなければ元の型のまま」という挙動になり、Promise でない値にも安全に使えます。取り出したい型の性質に応じて、偽の分岐を never にするか元の型にするかを選ぶとよいでしょう。

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

infer は関数型に対しても使えます。関数の戻り値の型を取り出すには、戻り値の位置に infer を置きます。これは標準ライブラリの ReturnType とほぼ同じ実装です。

function-types.ts
// 関数の戻り値の型を取り出す(ReturnType の中身とほぼ同じ)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// 関数の引数の型(タプル)を取り出す(Parameters の中身とほぼ同じ)
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;

type Fn = (id: number, name: string) => boolean;

type R = MyReturnType<Fn>;  // boolean
type P = MyParameters<Fn>;  // [id: number, name: string]

戻り値の位置に infer R を書けば戻り値の型が、引数の位置に infer P を書けば引数全体がタプル型として取り出せます。TypeScript に標準で用意されている ReturnType<T>Parameters<T> は、まさにこの仕組みで作られています。普段は標準のユーティリティ型を使えば十分ですが、内部でこう動いていると分かると、独自の型を組み立てるときにも応用が利きます。

これらの標準ユーティリティ型そのものの使い方は、別記事「ReturnTypeParameters の使い方」で詳しく扱っています。ここでは infer がその土台になっている、という点を押さえておけば十分です。

複数の infer と制約付きの infer

1 つの条件型の中に infer を複数書くこともできます。たとえば関数の最初の引数と戻り値を同時に取り出す、といった使い方です。

multiple-infer.ts
// 最初の引数の型 A と戻り値の型 R を同時に取り出す
type FirstArgAndReturn<T> =
  T extends (first: infer A, ...rest: any[]) => infer R
    ? { arg: A; result: R }
    : never;

type Fn = (id: number, name: string) => boolean;

type Info = FirstArgAndReturn<Fn>;  // { arg: number; result: boolean }

さらに、TypeScript 4.7 以降では infer に制約を付けられます。infer U extends string のように書くと、「マッチした型が string に代入できる場合だけ取り出す」という条件を加えられます。次の例は、タプルの先頭要素が文字列リテラルのときだけその値を取り出すものです。

constrained-infer.ts
// 先頭要素が string に代入できるときだけ、その型を取り出す
type FirstString<T> = T extends [infer U extends string, ...any[]] ? U : never;

type A = FirstString<["hello", 1, true]>;  // "hello"
type B = FirstString<[42, "world"]>;       // never(先頭が string ではない)

制約を付けると、条件を満たさない型を早い段階で弾けるほか、取り出した型変数を制約した型(この例では string)として扱えるようになり、後続の処理が書きやすくなります。

infer が使える場所とマッチしないときの挙動

infer は便利な反面、書ける場所や結果が直感に反することがあります。よくつまずく2点を整理しておきます。

Conditional Types の条件部分以外では使えない

inferextends の右側、つまり Conditional Types の条件を書く部分でしか使えません。型エイリアスの右辺にいきなり書いたり、条件型を使わずに単独で書いたりするとエラーになります。

where-infer.ts
// NG: Conditional Types の外では使えない
type Bad<T> = (infer U)[];  // エラー: 'infer' declarations are only permitted...

// OK: extends の右側(条件部分)でだけ使える
type Good<T> = T extends (infer U)[] ? U : never;

「型の一部を取り出したい」と思ったら、まず T extends ... ? ... : ... という条件型の枠を用意し、その条件の中に infer を置く、という順番を意識すると間違えにくくなります。

マッチしないと never になる

偽の分岐を never にしていると、パターンにマッチしなかったときに結果が never になります。「型を取り出したはずなのに never になってしまう」ときは、渡した型がパターンに合っていない可能性が高いです。たとえば配列を期待する条件に配列でない型を渡すと never になります。

また、条件型はユニオン型に対して各メンバーへ分配される(distributive になる)性質があり、想定と違う結果になることがあります。取り出した型が期待通りにならないときは、渡している型がユニオンになっていないか、パターンの構造が実際の型と一致しているかを確認してみてください。

infer と Conditional Types の対応を整理する

ここまで見てきた「取り出したい対象」と「そのために書くパターン」の対応を、まとめて確認しておきましょう。

取り出したいものinfer を使ったパターン
配列の要素型T extends (infer U)[] ? U : never
Promise の中身T extends Promise<infer U> ? U : T
関数の戻り値T extends (...args: any[]) => infer R ? R : never
関数の引数(タプル)T extends (...args: infer P) => any ? P : never
制約付きで取り出すT extends [infer U extends string, ...any[]] ? U : never

まとめ

infer は、Conditional Types の条件部分(extends の右側)でだけ使えるキーワードで、型の構造の中からマッチした部分を型変数として取り出す仕組みです。(infer U)[] で配列の要素型を、Promise<infer U> で Promise の中身を、関数型の戻り値や引数の位置に置けば ReturnTypeParameters と同じように型を取り出せます。複数の infer を同時に使ったり、infer U extends string のように制約を付けたりもできます。注意点としては、Conditional Types の外では使えないこと、パターンにマッチしないと never になること、ユニオン型では分配が起きることを覚えておくと、想定外の結果に悩まされにくくなります。まずは配列や Promise の単純な例から手を動かして、パターンマッチの感覚をつかんでみてください。

参考ページ