Extracting Types From Other Types
The infer keyword is the key to many TypeScript utility types and lets you extract partial types such as the resolved value of a Promise or the parameters of a function directly from an existing type without duplicating them by hand. Understanding how infer works inside conditional types lets you build your own utility types, avoid duplication, and keep derived types automatically in sync with their source.
Table of Contents
- 1. Why infer is indispensable for TypeScript developers
- 2. The mechanics: how infer works inside conditional types
- 3. Extracting Promise types: capturing the resolved value
- 4. Extracting function types: parameters and return values
- 5. Building your own ReturnType and Awaited utilities
- 6. Multiple infer positions and union distribution
- 7. Limits and pitfalls of infer
- 8. Practice: forms, API clients, and event systems
- 9. infer compared: manual vs. automatically derived
- 10. Summary
- 11. FAQ
1. Why infer is indispensable for TypeScript developers
Anyone coming from the Magento and PHP world knows this problem: a return type changes in one method, and ten other places in the code need to be updated manually by the caller. TypeScript solves this problem in theory through static typing, but without infer you fall into the same trap: you duplicate type definitions by hand, because there is no way to pull a sub-type out of an existing type. The infer keyword closes exactly that gap.
In practice this means: instead of retyping the return type of a function, the resolved value of a Promise, or the parameters of a callback signature, you let TypeScript derive the type itself. The result stays automatically in sync with its source, the compiler immediately flags any incompatible change, and utility types such as ReturnType, Parameters, or Awaited from the TypeScript standard library are internally built on exactly this mechanism.
2. The mechanics: how infer works inside conditional types
infer only exists inside the extends clause of a conditional type. The general form T extends U ? X : Y checks whether T structurally matches U. If you replace part of U with infer Name, you instruct TypeScript to declare a new type variable at exactly that position and fill it with whatever part of T structurally matches there. That variable is then available in the true branch (X), much like a capture group in a regular expression remembers the matching substring.
The order matters: the compiler first tries to match T against the structure containing the infer placeholder. If the pattern match fails, the false branch applies, and the inferred variable does not exist there. That is exactly what makes infer a structural tool: it describes a shape, not a concrete condition, and TypeScript resolves the unknown through unification against the actual type.
// Basic conditional type using infer to capture a sub-type
type ElementType<T> = T extends (infer U)[] ? U : never;
type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
type C = ElementType<boolean>; // never, boolean is not an array
// infer only works inside the "extends" clause of a conditional type
// T extends (infer U)[] ? U : never
// ^^^^^^^ captures the array element type as U
3. Extracting Promise types: capturing the resolved value
The classic starting example for infer is extracting the resolved value of a Promise. An async client often returns Promise<SomeResponse>, but many places in the code only need SomeResponse, for example to type a parameter or a field in a form. With T extends Promise<infer U> ? U : T you can pull out that inner type without any extra manual work, and the definition stays correct even if the return type of the original function later changes.
The naive version has a weakness, though: nested promises such as Promise<Promise<string>> only get unwrapped once. The built-in Awaited<T> utility type solves exactly this problem through recursion, calling itself again inside the true branch until no Promise remains. This recursive application of infer is a pattern that recurs across many advanced utility types.
// Extract the resolved type of a Promise
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type A = Unwrap<Promise<string>>; // string
type B = Unwrap<Promise<{ id: number }>>; // { id: number }
type C = Unwrap<number>; // number, not a promise, returned as-is
// Naive Unwrap does not resolve nested promises
type D = Unwrap<Promise<Promise<string>>>; // Promise<string>, still wrapped once
// Recursive version resolves nested promises fully, like the built-in Awaited<T>
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
type E = DeepUnwrap<Promise<Promise<string>>>; // string
4. Extracting function types: parameters and return values
Function signatures can be decomposed with infer just as precisely as promises. The pattern T extends (...args: infer P) => unknown ? P : never captures the entire parameter list as a tuple, including names and optional parameters, because TypeScript understands rest parameter syntax at the type level just as it does at runtime. This is especially useful when building a wrapper around an existing function and you want the wrapper's signature to adapt automatically whenever the original function changes.
You can shift the position of the infer placeholder deliberately to capture only part of the parameters, for example just the first one via (first: infer F, ...rest: unknown[]) => unknown. This precision is what distinguishes infer from a plain copy of the signature: you describe exactly which slice of the structure matters, and deliberately leave the rest of the structure to the unknown placeholder.
// Extract the parameter tuple of a function type
type Params<T> = T extends (...args: infer P) => unknown ? P : never;
function createOrder(customerId: string, items: string[], priority: boolean) {
return { customerId, items, priority };
}
type CreateOrderParams = Params<typeof createOrder>;
// [customerId: string, items: string[], priority: boolean]
// Extract just the first parameter
type FirstParam<T> = T extends (first: infer F, ...rest: unknown[]) => unknown
? F
: never;
type FirstArg = FirstParam<typeof createOrder>; // string
5. Building your own ReturnType and Awaited utilities
The best way to understand infer is to rebuild the built-in utility types yourself once. A simplified version of ReturnType<T> only needs a constraint that ensures T is actually a function, plus a conditional type that captures the return type via infer R at the position after the arrow. The built-in type in lib.es5.d.ts is barely different from this minimal implementation; at its core it is exactly this one pattern.
For Awaited<T> you combine infer with recursion: the conditional type checks whether the inferred type U is itself another Promise, and if so calls itself again with U instead of returning immediately. This self-reference has been explicitly allowed in TypeScript for several versions now, and the compiler caps it with a recursion depth limit to prevent infinite loops in the type system.
// A simplified version of the built-in ReturnType<T> utility
type MyReturnType<T extends (...args: never[]) => unknown> =
T extends (...args: never[]) => infer R ? R : never;
function fetchOrder(id: string) {
return { id, total: 42, currency: "EUR" };
}
type Order = MyReturnType<typeof fetchOrder>;
// { id: string; total: number; currency: string }
// A simplified version of the built-in Awaited<T> utility
type MyAwaited<T> = T extends Promise<infer U>
? U extends Promise<unknown>
? MyAwaited<U>
: U
: T;
async function fetchOrderAsync(id: string) {
return fetchOrder(id);
}
type AsyncOrder = MyAwaited<ReturnType<typeof fetchOrderAsync>>;
// { id: string; total: number; currency: string }
6. Multiple infer positions and union distribution
A conditional type is not limited to a single infer. T extends (...args: infer P) => infer R ? [P, R] : never captures both the parameters and the return type in a single pass and returns them as a tuple. Each infer position is resolved independently against the matching part of T's structure, as long as the overall pattern structurally matches T. This enables compact utility types that pull several pieces of information out of a single signature at once, without walking the type twice.
One special case concerns distributive conditional types: when a bare type parameter sits directly before extends, TypeScript automatically distributes the conditional type over every member of a union. With type Wrapped<T> = T extends Promise<infer U> ? U : T applied to Promise<string> | Promise<number>, infer is evaluated separately for each union member, and the result is the union string | number, not a single unresolved type.
7. Limits and pitfalls of infer
infer is strictly tied to the extends branch of a conditional type and cannot appear anywhere else, not in a standalone type declaration and not in an interface. The most common beginner mistake is trying to use infer outside that context, which TypeScript rejects with a clear compiler error. A second stumbling block: if the same infer variable appears multiple times in contravariant position, for example across several function parameters, the compiler builds an intersection instead of a union, which can produce surprisingly narrow types.
Diagnosing errors is also harder than with ordinary type errors: when the structure doesn't match, you often end up in the false branch and therefore at never, without a meaningful error message pointing at the actual root cause. Deeply nested, recursive infer constructions can also noticeably slow down type checking and, in extreme cases, exceed the recursion depth TypeScript enforces internally. A good compromise is to split complex infer chains into named intermediate steps instead of packing everything into one barely readable type expression.
8. Practice: forms, API clients, and event systems
In practice, infer rarely appears in isolation, it's usually embedded inside larger utility types of production libraries. Form libraries derive the type of a field value from a validation schema, type-safe API clients extract the response type directly from an endpoint's definition, and event systems capture a listener's payload type without writing it out again at the call site. In all three cases, infer keeps runtime behavior and type definitions from drifting apart.
A typical example is a typed event bus where a listener's payload type is derived automatically from its function signature instead of being declared separately. When you register a listener for a specific event, TypeScript already knows the exact shape of the payload thanks to infer and mapped types, including editor autocompletion. That not only saves typing, it prevents an entire class of bugs where the payload and the listener signature silently drift apart.
// Practical example: a typed event bus derives payload types via infer
type EventMap = {
"order:created": { orderId: string; total: number };
"order:cancelled": { orderId: string; reason: string };
};
type Listener<T> = (payload: T) => void;
// Extract the payload type straight out of the listener signature
type PayloadOf<L> = L extends Listener<infer P> ? P : never;
class TypedEventBus<Events extends Record<string, unknown>> {
private listeners: { [K in keyof Events]?: Listener<Events[K]>[] } = {};
on<K extends keyof Events>(event: K, listener: Listener<Events[K]>): void {
(this.listeners[event] ??= []).push(listener);
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
this.listeners[event]?.forEach((listener) => listener(payload));
}
}
const bus = new TypedEventBus<EventMap>();
bus.on("order:created", (payload) => {
// payload is inferred as { orderId: string; total: number }
console.log(payload.orderId);
});
9. infer compared: manual vs. automatically derived
The table below contrasts the manual approach, where types are duplicated by hand, with the infer-based approach, where TypeScript derives the type automatically from its source. The difference looks small at first glance, but it has a direct impact on the maintainability of larger codebases.
| Use case | Manually duplicated | Derived with infer | Advantage |
|---|---|---|---|
| Promise resolved type | type Result = User; | Awaited<ReturnType<typeof fn>> | Stays correct when the signature changes |
| Function return type | type Return = Order; | ReturnType<typeof fn> | No duplication, always up to date |
| Function parameters | type Args = [string, number]; | Parameters<typeof fn> | Also captures optional parameters |
| Array element type | type Item = Product; | T extends (infer U)[] ? U : never | Works generically for any array |
| Object value type | type Value = string; | T extends Record<string, infer V> ? V : never | Adapts automatically to new fields |
The same principle applies to every row in the table: the manual approach only works until the source changes and nobody remembers to update the copy. The infer-based approach, in contrast, stays automatically correct, because the derived type is structurally tied to its source and updates itself with every change.
Mironsoft
TypeScript code review, utility type refactoring, and type-safe API clients for Magento and Hyvä projects
Ready for type-safe TypeScript code?
We review your TypeScript code, build maintainable utility types with infer and conditional types, and make sure your API clients and forms stay type-safe end to end, from the Magento integration to the frontend build.
TypeScript code review
Consistent, maintainable type definitions instead of duplicates and any
Utility type refactoring
Custom infer-based utility types instead of copy-paste types
Type-safe API clients
Response and request types derived automatically from schemas
10. Summary
The infer keyword solves a very concrete problem: deriving types from existing types instead of duplicating them by hand. It only works inside the extends clause of a conditional type, where, like a capture group, it declares a type variable at a specific structural position and fills it with the matching part of the type being examined. The built-in utility types ReturnType, Parameters, and Awaited are, at their core, nothing more than compact, well-tested applications of exactly this mechanism.
Anyone building their own utility types with infer should keep three things in mind: know the limits of the mechanism, so never use infer outside a conditional type, account for the effects of distributive evaluation on union types, and split complex constructions into named, readable intermediate steps. Used correctly, infer keeps derived types automatically in sync with their source, without any extra maintenance effort whenever something changes.
The infer Keyword - The Essentials at a Glance
Mechanics
infer declares a type variable only inside the extends clause of a conditional type and captures the matching sub-structure.
Promise & Awaited
The resolved value of a Promise can be extracted with infer, nested promises need a recursive variant like Awaited.
Functions
A function's parameters and return type can be captured individually or together via multiple infer positions.
Limits
infer does not work standalone, distributes automatically over unions, and should be split into intermediate steps in complex chains.