Dynamic Type Inference from API Responses using TypeScript's infer Keyword
Min-jun Kim
Dev Intern · Leapcell

Introduction
In the world of modern web development, interacting with APIs is a daily reality. We send requests, and we receive responses. While the data itself is crucial, ensuring that our local application understands the precise shape of that data is equally vital for robust, type-safe code. Manually defining interfaces for every single API response can quickly become a tedious and error-prone task, especially when dealing with complex or evolving APIs. This often leads to developers either maintaining brittle, hand-typed definitions or, worse, relying on any, sacrificing type safety entirely.
However, TypeScript offers a powerful feature, the infer keyword, which can revolutionize how we handle API response types. By leveraging infer, we can dynamically extract and deduce the return type directly from an API function's signature, enabling us to build more resilient, less boilerplate-heavy applications. This article will delve into the practical application of infer to elegantly solve the common problem of dynamically inferring return types from API responses, significantly improving developer experience and code maintainability.
Core Concepts and Principles
Before we dive into the practical implementation, let's briefly define some key TypeScript concepts that underpin our solution:
- Generics: Generics allow us to write flexible, reusable code that works with different types while maintaining type safety. You'll often see them denoted by angle brackets, like
Array<T>orPromise<T>. - Conditional Types: These types allow us to choose a type based on a condition. Their syntax resembles a ternary operator:
Condition ? TypeIfTrue : TypeIfFalse. inferKeyword: This is the star of our show. Within a conditional type,inferallows us to "capture" a type that is part of another type and then use that captured type in the true branch of the conditional type. It's a powerful mechanism for pattern matching on types. For example,T extends Promise<infer U> ? U : Twould extract the resolved typeUfrom aPromise<U>.ReturnType<T>Utility Type: This built-in TypeScript utility type extracts the return type of a function typeT. For instance,ReturnType<() => string>would resolve tostring. While useful, it doesn't directly help us with promises which is what most API calls return.Awaited<T>Utility Type: Introduced in TypeScript 4.5,Awaited<T>extracts the awaited type of aPromise<T>or recursively unwraps nested promises. This is particularly relevant for API calls that return promises.
Dynamic Type Inference in Action
Our goal is to create a utility type that, given an asynchronous API function (which typically returns a Promise), can effectively "unwrap" that promise and give us the type of the data it resolves to.
Let's imagine we have a simple API function:
// 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> { // Simulate API call return { id: userId, name: 'John Doe', email: 'john@example.com' }; } async function fetchProducts(): Promise<Product[]> { // Simulate API call return [{ productId: 'P1', productName: 'Laptop', price: 1200 }]; }
Now, let's build our InferApiResponse utility type using infer:
// utils.ts type InferApiResponse<T extends (...args: any[]) => Promise<any>> = T extends (...args: any[]) => Promise<infer R> ? R : never;
Let's break down this InferApiResponse type:
T extends (...args: any[]) => Promise<any>: This is a constraint. It ensures that the typeTpassed toInferApiResponsemust be a function that returns aPromiseof anything. This is crucial because we're specifically targeting asynchronous API functions.T extends (...args: any[]) => Promise<infer R>: This is the conditional type where the magic happens.- We are checking if
T(our API function type) can be assigned to a function type that returns aPromise. - Crucially,
infer Rtells TypeScript: "If the return type of this function is aPromise, please infer the type that the promise resolves to and assign it to the new type variableR."
- We are checking if
? R : never:- If the condition is true (i.e.,
Tis a function returning aPromise, and we successfully inferredR), then ourInferApiResponsetype will resolve toR(the resolved type of the promise). - If the condition is false (which shouldn't happen if our initial constraint is met), we fall back to
never, indicating an impossible type.
- If the condition is true (i.e.,
Let's see it in action:
// app.ts import { fetchUser, fetchProducts } from './api'; import { InferApiResponse } from './utils'; // Assuming utils.ts is where InferApiResponse is defined type UserApiResponse = InferApiResponse<typeof fetchUser>; // UserApiResponse will correctly be inferred as User type ProductsApiResponse = InferApiResponse<typeof fetchProducts>; // ProductsApiResponse will correctly be inferred as Product[] // Example usage: async function displayUser(userId: number) { const user: UserApiResponse = await fetchUser(userId); console.log(user.name); // Type-safe access // user.id, user.email are also available with correct types } async function displayProducts() { const products: ProductsApiResponse = await fetchProducts(); console.log(products[0].productName); // Type-safe access // products[0].productId, products[0].price are also available } displayUser(1); displayProducts();
As you can see, UserApiResponse is automatically inferred as User, and ProductsApiResponse is inferred as Product[], completely eliminating the need to manually re-type these interfaces for consumption in other parts of our application.
Why not just Awaited<ReturnType<typeof fetchUser>>?
One might wonder why we don't just use Awaited<ReturnType<typeof fetchUser>>. This also works and arguably is more concise for simple cases:
type UserApiResponse2 = Awaited<ReturnType<typeof fetchUser>>; // Also resolves to User
While Awaited<ReturnType<T>> works perfectly for this specific scenario, InferApiResponse using infer is more fundamental and demonstrates the power of infer for more complex type manipulations where Awaited or ReturnType alone might not suffice, or where you need to extract types from more complex structures than just a function's return. Our custom InferApiResponse also enforces the constraint of accepting only functions that return promises, which can be a useful guard.
The infer keyword excels in situations where you need to pattern match on types and extract a specific part of that pattern. Awaited and ReturnType are specialized utility types built using similar concepts (likely infer internally) for common patterns. Understanding infer gives you the flexibility to build your own specialized utility types for unique scenarios.
Conclusion
The infer keyword in TypeScript is a remarkably powerful tool for dynamic type inference, especially when dealing with API responses. By creating a simple utility type, we can automatically deduce the precise structure of the data returned by asynchronous API functions, drastically reducing boilerplate and significantly enhancing type safety across our applications. This approach not only streamlines development but also provides greater confidence in our code as APIs evolve, making our JavaScript projects more robust and maintainable. Embracing infer helps us write smarter, safer, and more expressive TypeScript code by letting the compiler do the heavy lifting of type deduction.

