TypeScriptのinferキーワードを用いたAPIレスポンスからの動的な型推論
Min-jun Kim
Dev Intern · Leapcell

はじめに
現代のWeb開発の世界では、APIとのやり取りは日常的な現実です。リクエストを送信し、レスポンスを受信します。データ自体が重要であると同時に、ローカルアプリケーションがそのデータの正確な形状を理解していることを保証することは、堅牢で型安全なコードにとって等しく重要です。すべてのAPIレスポンスに対して手動でインターフェースを定義することは、特に複雑または進化するAPIを扱う場合、すぐに退屈でエラーが発生しやすい作業になります。これはしばしば、開発者が壊れやすい手動で型付けされた定義を維持するか、さらに悪いことに、anyに頼って型安全性を完全に犠牲にすることにつながります。
しかし、TypeScriptはinferキーワードという強力な機能を提供しており、APIレスポンスの型を処理する方法に革命をもたらすことができます。inferを活用することで、API関数のシグネチャから直接返される型を動的に抽出し推論することができ、より回復力があり、ボイラープレートの少ないアプリケーションを構築できるようになります。この記事では、APIレスポンスからの返される型の動的推論という一般的な問題をエレガントに解決するためにinferを実践的に適用する方法を掘り下げ、開発者エクスペリエンスとコードの保守性を大幅に向上させます。
コアコンセプトと原則
実用的な実装に入る前に、ソリューションの基盤となるいくつかの主要なTypeScriptの概念を簡単に定義しましょう。
- ジェネリクス: ジェネリクスにより、型安全性を維持しながら、さまざまな型で機能する柔軟で再利用可能なコードを書くことができます。これらはしばしば角括弧で示されます。例:
Array<T>またはPromise<T>。 - 条件付き型: これらの型は、条件に基づいて型を選択できます。構文は三項演算子に似ています:
Condition ? TypeIfTrue : TypeIfFalse。 inferキーワード: これは私たちの主役です。条件付き型内では、inferは別の型の一部である型を「キャプチャ」し、そのキャプチャされた型を条件付き型のtrueブランチで使用できるようにします。これは型に対するパターンマッチングのための強力なメカニズムです。たとえば、T extends Promise<infer U> ? U : TはPromise<U>から解決された型Uを抽出します。ReturnType<T>ユーティリティ型: この組み込みTypeScriptユーティリティ型は、関数型Tの返される型を抽出します。たとえば、ReturnType<() => string>はstringに解決されます。これは便利ですが、ほとんどのAPI呼び出しが返すPromiseには直接役立ちません。Awaited<T>ユーティリティ型: TypeScript 4.5で導入されたAwaited<T>は、Promise<T>の待機された型を抽出するか、ネストされたPromiseを再帰的にアンラップします。これはPromiseを返すAPI呼び出しに特に重要です。
APIレスポンスの動的な型推論の実践
私たちの目標は、非同期API関数(通常はPromiseを返す)が与えられたときに、そのPromiseを効果的に「アンラップ」して、それが解決されるデータの型を取得できるユーティリティ型を作成することです。
簡単なAPI関数を想像してみましょう。
// api.ts interface User { id: number; name: string; email: string; } interface Product { productId: string; productName: string; price: number; } async function fetchUser(userId: number): Promise<User> { // API呼び出しをシミュレート return { id: userId, name: 'John Doe', email: 'john@example.com' }; } async function fetchProducts(): Promise<Product[]> { // API呼び出しをシミュレート return [{ productId: 'P1', productName: 'Laptop', price: 1200 }]; }
次に、inferを使用してInferApiResponseユーティリティ型を構築しましょう。
// utils.ts type InferApiResponse<T extends (...args: any[]) => Promise<any>> = T extends (...args: any[]) => Promise<infer R> ? R : never;
このInferApiResponse型を分解してみましょう。
T extends (...args: any[]) => Promise<any>: これは制約です。InferApiResponseに渡される型TがPromiseを返す関数でなければならないことを保証します。これは、非同期API関数を特にターゲットにしているため、重要です。T extends (...args: any[]) => Promise<infer R>: ここが魔法が起こる条件付き型です。T(API関数型)がPromiseを返す関数型に割り当て可能かどうかをチェックしています。- 決定的に、
infer RはTypeScriptに次のように指示します。「この関数の返される型がPromiseの場合、Promiseが解決する型を推論し、それを新しい型変数Rに割り当ててください。」
? R : never:- 条件がtrueの場合(つまり、
TがPromiseを返す関数であり、Rを正常に推論できた場合)、InferApiResponse型はR(Promiseの解決された型)に解決されます。 - 条件がfalseの場合(初期制約が満たされていれば起こらないはずです)、
neverにフォールバックし、不可能な型を示します。
- 条件がtrueの場合(つまり、
実際に使用してみましょう。
// app.ts import { fetchUser, fetchProducts } from './api'; import { InferApiResponse } from './utils'; // InferApiResponseが定義されている場所を utils.ts と仮定 type UserApiResponse = InferApiResponse<typeof fetchUser>; // UserApiResponse は正しく User として推論されます type ProductsApiResponse = InferApiResponse<typeof fetchProducts>; // ProductsApiResponse は正しく Product[] として推論されます // 使用例: async function displayUser(userId: number) { const user: UserApiResponse = await fetchUser(userId); console.log(user.name); //型安全なアクセス // user.id, user.email も正しい型で利用可能です } async function displayProducts() { const products: ProductsApiResponse = await fetchProducts(); console.log(products[0].productName); //型安全なアクセス // products[0].productId, products[0].price も利用可能です } displayUser(1); displayProducts();
ご覧のとおり、UserApiResponseは自動的にUserとして推論され、ProductsApiResponseはProduct[]として推論されます。これにより、アプリケーションの他の部分でこれらのインターフェースを消費するために手動で型を再入力する必要がなくなります。
なぜ Awaited<ReturnType<typeof fetchUser>> ではないのか?
この特定のシナリオでは、Awaited<ReturnType<typeof fetchUser>>を使用しないのはなぜか疑問に思うかもしれません。これはまさに機能し、おそらくより簡潔です。
type UserApiResponse2 = Awaited<ReturnType<typeof fetchUser>>; // User に解決されます
Awaited<ReturnType<T>>は、この特定のシナリオでは完璧に機能しますが、inferを使用したInferApiResponseは、AwaitedやReturnTypeだけでは十分でない、またはPromiseの返り値よりも複雑な構造から型を抽出する必要がある、より複雑な型操作に対してはさらに基本的であり、inferの力を実証しています。私たちのカスタムInferApiResponseは、Promiseを返す関数のみを受け入れる制約を強制します。これは有用なガードとなり得ます。
inferキーワードは、型に対してパターンマッチングを実行し、そのパターンの特定の部分を抽出する必要がある状況で優れています。AwaitedとReturnTypeは、(おそらく内部的にinferを使用している)同様の概念を使用して構築された特殊なユーティリティ型です。inferを理解することで、一般的なパターンのために構築された独自の特殊なユーティリティ型を作成する柔軟性が得られます。
結論
TypeScriptのinferキーワードは、特にAPIレスポンスを扱う際に、動的な型推論のための驚くほど強力なツールです。単純なユーティリティ型を作成することで、非同期API関数によって返されるデータの正確な構造を自動的に推論し、ボイラープレートを大幅に削減し、アプリケーション全体の型安全性を大幅に向上させることができます。このアプローチは開発を合理化するだけでなく、APIが進化するにつれてコードに対する自信を高め、JavaScriptプロジェクトをより堅牢で保守しやすくします。inferを採用することで、コンパイラに型推論の重労働を任せることで、よりスマートで、より安全で、より表現力豊かなTypeScriptコードを書くことができます。

