【実務・中級編】 inferキーワードによる型推論 – TypeScript実践ガイド

おう、元気か? 今日のテーマはTypeScriptの `infer` キーワードだ。これ、Conditional Typesと組み合わせると、もうコードの書き方が一段階も二段階も変わる、まさに「魔法」みたいなもんだ。

「いやいや、そんな大げさな」って思ったか? まあ、落ち着け。まずはこの `infer` ってやつが何者なのか、そしてどういう場面で「なるほど!」ってなるのか、実務でよくあるケースを具体的に見ていこうぜ。

1. `infer` キーワードって、一体何者なんだ?

簡単に言うと、`infer` は「型推論の推論」だ。Conditional Typesってのは、「もしAという型だったらB、そうでなければC」みたいな条件分岐を書くためのものだろ? で、その「Aという型だったら」の部分で、さらに「その型の中身を一旦保留しておいて、後で使う」っていうのが `infer` の役割なんだ。

例えば、こんな感じだ。

// Tは、何かしらの型が入る変数だと思ってくれ
type MyReturnType = T extends (…args: any[]) => infer R ? R : never;

この `MyReturnType` っていう型を分解してみよう。

  • `T extends (…args: any[]) => infer R`
  • 「もし `T` が関数型だったら…」
  • 「その関数の戻り値の型を `R` という名前で一旦保留しておく(推論する)」
  • `? R`
  • 「もし上記が真なら、保留しておいた `R` (戻り値の型) を返す」
  • `: never`
  • 「もし上記が偽(つまり関数型でなかったら)、`never` 型を返す」

どうだ? `infer R` の部分で、関数 `T` の戻り値の型を「推論」して、それを `R` という変数に格納してる。そして、その `R` をそのまま結果として返してるんだ。

これは、TypeScriptの標準ライブラリでもガンガン使われてるテクニックだ。例えば、Promiseの内部の型を抽出したり、配列の要素の型を取り出したり。これを知ってると、自分でユーティリティ型をゴリゴリ書くときに、ものすごい武器になる。

2. 実務で「これ、`infer` じゃないと無理だろ!」ってなるケース

じゃあ、具体的にどんな場面で `infer` が活躍するのか、いくつか例を見てみよう。

2.1. 関数の戻り値の型を抽出したい

これが一番よく見るパターンかな。ある関数型があって、その「戻り値の型だけ」が欲しい時。

例えば、こんな関数があるとしよう。

function getUserName(id: number): string {
// … 実際にはAPIを叩いたりする処理 …
return `User_${id}`;
}

function getUserInfo(id: number): { name: string; age: number } {
// …
return { name: `User_${id}`, age: 30 };
}

ここで、`getUserName` の戻り値の型(`string`)だけが欲しい。`getUserInfo` の戻り値の型(`{ name: string; age: number }`)だけが欲しい。

`infer` を使わないと、いちいち `string` とか `{ name: string; age: number }` と手で書かないといけない。でも、もし関数のシグネチャが変わったら? そのたびに型定義も修正しないといけないのは面倒だ。

そこで、`infer` の出番だ。

// infer を使って関数の戻り値の型を抽出するユーティリティ型
type GetReturnType = T extends (…args: any[]) => infer R ? R : never;

// 関数型を直接渡して、戻り値の型だけを取り出す
type UserNameType = GetReturnType; // string になる
type UserInfoType = GetReturnType; // { name: string; age: number } になる

// 型を確認してみよう
const userName: UserNameType = “Alice”;
const userInfo: UserInfoType = { name: “Bob”, age: 25 };

console.log(userName, userInfo);

`typeof getUserName` ってやって、型レベルで関数そのものを参照してるのがポイントだ。そして、`T extends (…args: any[]) => infer R` の部分で、もし `T` が関数だったら、その戻り値の型を `R` として推論してくれる。

2.2. 配列の要素の型を抽出したい

これもよくある。「ある配列型があって、その配列の要素の型だけ」が欲しい時。

例えば、こんな型定義があったとする。

type UserList = { id: number; name: string }[];
type ProductList = string[];

この `UserList` から `{ id: number; name: string }` を、`ProductList` から `string` を取り出したい。

// infer を使って配列の要素の型を抽出するユーティリティ型
type GetArrayElementType = T extends (infer R)[] ? R : never;

// 型を抽出してみる
type UserElementType = GetArrayElementType; // { id: number; name: string } になる
type ProductElementType = GetArrayElementType; // string になる

// 型を確認してみよう
const userElement: UserElementType = { id: 1, name: “Charlie” };
const productElement: ProductElementType = “Apple”;

console.log(userElement, productElement);

`T extends (infer R)[]` の部分が肝だ。これは「もし `T` が `R` 型の要素をいくつも持つ配列だったら」という意味になる。`infer R` で配列の要素の型 `R` を推論し、それを返している。

2.3. Promise の中身の型を抽出したい

非同期処理でよく使う `Promise` 型。`Promise` の `T` の部分だけを取り出したい、なんてこともよくある。

// infer を使って Promise の中身の型を抽出するユーティリティ型
type GetPromiseValue = T extends Promise ? R : never;

// 例: 非同期処理の戻り値の型
async function fetchUserData(): Promise<{ userId: number; userName: string }> {
// … API 呼び出し …
return { userId: 123, userName: “David” };
}

// fetchUserData の戻り値の型は Promise<{ userId: number; userName: string }>
// ここから中身の型だけを取り出す
type UserDataType = GetPromiseValue>;
// UserDataType は { userId: number; userName: string } になる

// 型を確認してみよう
const userData: UserDataType = { userId: 456, userName: “Eve” };

console.log(userData);

`T extends Promise` の部分で、「もし `T` が `Promise` という型だったら」となり、`infer R` が `Promise` の中身の型を推論してくれる。

`ReturnType` は、`fetchUserData` 関数の戻り値の型全体、つまり `Promise<{ userId: number; userName: string }>` を取得する標準のユーティリティ型だ。それに `GetPromiseValue` を適用することで、目的の型 ` { userId: number; userName: string }` を取り出せる。

3. `infer` を使う上での注意点とベストプラクティス

`infer` は強力だけど、いくつか気をつけておきたいこともある。

3.1. `infer` は Conditional Types の中でしか使えない

これは仕様だから仕方ない。`infer` は「条件付きの型推論」だから、その条件分岐がないと意味がないんだ。

3.2. `infer` で推論できる型は一つだけ

一つの `infer` キーワードで推論できる型は、その部分で表される型だけだ。複数の型を同時に推論したい場合は、複数回 `infer` を使うか、タプル型などを活用してまとめて推論する必要がある。

例えば、関数の引数と戻り値の両方を推論したい場合。

// infer を使って引数と戻り値の型を同時に取得しようとする例(少し複雑)
type GetArgsAndReturn = T extends (…args: infer Args) => infer Return ? { args: Args; return: Return } : never;

function exampleFn(a: number, b: string): boolean {
return true;
}

type FnInfo = GetArgsAndReturn;
// FnInfo は { args: [a: number, b: string]; return: boolean; } になる

const args: FnInfo[‘args’] = [10, “hello”];
const ret: FnInfo[‘return’] = false;

console.log(args, ret);

ここでは、`infer Args` で引数の型(タプル型になる)を推論し、`infer Return` で戻り値の型を推論している。

3.3. 型の「名前」ではなく「構造」で判断する

`infer` は、渡されてきた型が「どういう構造になっているか」に基づいて推論する。だから、`string` 型を `string` と推論するわけじゃなくて、「文字列リテラルの型」とか「文字列型の配列」みたいな、その型が持つ構造を捉えてくれる。

3.4. 複雑すぎる型推論は可読性を下げる可能性も

`infer` を駆使すると、ものすごく複雑な型を動的に定義できる。それはそれで素晴らしいんだけど、あまりにも複雑になりすぎると、他の開発者が読みにくくなる可能性がある。

チームで開発しているときは、その「複雑さ」と「得られるメリット」のバランスをよく考えることが大事だ。場合によっては、素直に型を定義した方が分かりやすいこともある。

4. まとめ: `infer` は TypeScript の「関節」を柔らかくする

今日の話、どうだった? `infer` キーワードは、Conditional Typesと組み合わせることで、TypeScriptの型システムに「柔軟性」と「動的な推論能力」をもたらしてくれる、まさに縁の下の力持ちだ。

  • 関数の戻り値型
  • 配列の要素型
  • Promise の中身の型

これらを、元の型定義に依存せずに動的に抽出できるようになる。これにより、コードの再利用性が高まり、型定義のメンテナンスコストも下げられる。

最初はちょっと難しく感じるかもしれないけど、一度この「型推論の推論」という考え方に慣れてしまえば、TypeScriptのコードがもっと面白く、もっとパワフルに書けるようになるはずだ。

ぜひ、今日紹介したサンプルコードを自分のプロジェクトで試してみてくれ。きっと「なるほど!」って思える瞬間が訪れるはずだから。

じゃあ、またな!

コメント

タイトルとURLをコピーしました