Infer keyword in TypeScript — infer TypeScript
infer is a TypeScript keyword that captures and names a type from inside a conditional type match, letting you pull out parts of a type structure without knowing them in advance.
Theory
TL;DR
- Think of
inferas a regex capture group for types: you describe the shape, TypeScript fills in the matched part - Works only inside the
extendsclause of a conditional type - The captured type is available only in the true branch
- Use it when a nested type repeats across your API and you want to extract it once
- Skip it when the type is already known: use indexed access
T['key']instead
Quick example
// Extract return type from any function
type FnReturn<T> = T extends (...args: any[]) => infer R ? R : never;
type AddReturn = FnReturn<(a: number, b: number) => number>; // number
type StringReturn = FnReturn<() => string>; // string
type NotAFn = FnReturn<string>; // neverThe infer R part tells TypeScript: "whatever sits in the return position, capture it as R." If the match succeeds, R is the return type. If it fails, the false branch returns never.
How infer differs from direct type access
Direct access like T['returnType'] works when the property name is known. infer works when the structure is known but the inner type is not. You can pattern-match a function signature, an array, or a Promise and extract the type sitting inside, regardless of what that type turns out to be. That is the key difference: infer captures types from structural positions, not from named keys.
Without infer, extracting a function's return type would require either hardcoding the type or passing it as a separate generic parameter. Both approaches break the moment the function is provided externally.
When to use
- Extract function return type:
T extends (...args: any[]) => infer R ? R : never - Pull element type from an array:
T extends readonly (infer U)[] ? U : never - Unwrap a Promise:
T extends Promise<infer U> ? U : never - Capture function parameters as a tuple:
T extends (...args: infer A) => any ? A : never - Avoid
inferwhen the type is already known: useT[0]for a known tuple position,T['value']for a known property
How the compiler handles infer
TypeScript resolves infer during type-checking, not at runtime. When the compiler evaluates a conditional type, it tries to unify the input against the extends pattern. If the match succeeds, it binds infer R to the type that occupies that structural position, then substitutes R into the true branch. This all happens at compile time inside the type mapper, similar to how generic parameters are instantiated. No code is generated. No runtime overhead. The bound variable exists only within the true branch of that specific conditional.
Common mistakes
Mistake 1: Using infer outside a conditional type
// infer only exists inside an extends clause
type Bad<T> = infer R; // Error: 'infer' declarations are only permitted in the 'extends' clauseinfer is not a standalone keyword. It has no meaning outside the extends clause of a conditional type.
Mistake 2: Expecting the captured type in the false branch
type Foo<T> = T extends { x: infer U } ? U : U; // Error - U is not in scope hereU exists only in the true branch. Once the match fails and TypeScript takes the false path, the captured variable is gone.
Mistake 3: Forgetting distribution over unions
type ElementType<T> = T extends (infer U)[] ? U : never;
type Result = ElementType<string[] | number[]>; // string | numberTypeScript distributes conditional types over unions, applying the conditional to each member separately and then combining the results. This is usually what you want, but it can surprise you when one union member does not match the pattern.
Mistake 4: Capturing function parameters by exact position instead of rest
// Fragile: only matches functions with exactly two params
type TwoArgReturn<T> = T extends (a: infer A, b: infer B) => infer R ? R : never;
// Better: captures all params as a tuple regardless of count
type Params<T> = T extends (...args: infer A) => any ? A : never;Using ...args: infer A gives you a tuple of all parameters in one shot. Matching positional parameters one by one only works if the function has a fixed, known signature.
Mistake 5: Expecting infer to do something at runtime
infer is erased completely during compilation. If you need to inspect or validate types at runtime, you need a different tool: typeof operators, type guards, or a schema library like Zod.
Real-world usage
- Built-in utilities:
ReturnType<T>,Parameters<T>, andAwaited<T>are all built oninferin TypeScript's standard library - React/Redux:
ReturnType<typeof useSelector>extracts the selector's return type, giving you the store slice type without writing it twice - Zod:
z.infer<typeof schema>pulls the validated TypeScript type from a schema definition - tRPC:
inferRouterOutputs<AppRouter>gives you typed API responses derived directly from your router - TanStack Query: hook generics use
infer-based patterns to carry the data type through the query pipeline
Follow-up questions
Q: Write a type that extracts the arguments tuple from any function.
A: type Params<T> = T extends (...args: infer A) => any ? A : never; The rest syntax ...args captures all parameters as a single typed tuple, regardless of how many there are.
Q: What does the false branch return when infer does not match?
A: Whatever you put there explicitly, typically never. The infer variable simply does not exist in the false branch. There is nothing to fall back on.
Q: How does infer behave when the input is a union type?
A: TypeScript distributes the conditional over each union member separately. ElementType<string[] | number[]> evaluates ElementType<string[]> and ElementType<number[]> independently, then unions the results to give string | number.
Q: Can you use infer to recursively unwrap nested Promises?
A: Yes. type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T; keeps unwrapping until the innermost type is not a Promise. Recursive conditional types require TypeScript 4.1 or later.
Q: What is the difference between infer and mapped types?
A: Mapped types iterate over known keys and transform them. infer pattern-matches against a structural shape to capture an unknown inner type. They solve completely different problems and are often used together in advanced utility types.
Q: (Senior) Can infer capture types from intersection types the same way it does from unions?
A: No. Intersections do not distribute like unions do. T extends A & B with infer placed inside B does not give you isolated access to what A contributes. You typically need separate conditional types for each part and then intersect the captured results manually.
Examples
Extracting array element type
type ElementType<T> = T extends readonly (infer U)[] ? U : never;
type NumArr = ElementType<number[]>; // number
type Mixed = ElementType<readonly [string, boolean]>; // string | boolean
type NotArray = ElementType<string>; // neverThe readonly in the pattern is important. Without it, passing a readonly string[] would fall through to never because the type does not match a mutable array. Adding readonly makes the pattern accept both mutable arrays and readonly tuples.
Function parameter extraction
type HookParams<T> = T extends (...args: infer A) => any ? A : never;
declare function useDebounce(value: string, delay: number): string;
type DebounceParams = HookParams<typeof useDebounce>;
// [value: string, delay: number]
// Wrap any function without repeating its signature:
function withLogging<T extends (...args: any[]) => any>(
fn: T,
...args: HookParams<T>
): ReturnType<T> {
console.log("calling with", args);
return fn(...args);
}This pattern is particularly useful when wrapping third-party hooks or middleware. If useDebounce changes its signature in a library update, HookParams picks it up automatically without any manual type updates.
Recursive Promise unwrapping
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type A = DeepAwaited<Promise<string>>; // string
type B = DeepAwaited<Promise<Promise<number>>>; // number
type C = DeepAwaited<Promise<Promise<() => boolean>>>; // () => boolean
type D = DeepAwaited<string>; // string (passthrough)This is essentially how TypeScript's built-in Awaited<T> utility works under the hood. The recursive call keeps unwrapping until the innermost type is no longer a Promise. I reached for this pattern on a project where API response types were nested two or three levels deep across different service wrappers and the types kept drifting between team members once written manually.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.