Skip to main content

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 infer as a regex capture group for types: you describe the shape, TypeScript fills in the matched part
  • Works only inside the extends clause 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

typescript
// 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>; // never

The 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 infer when the type is already known: use T[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

typescript
// infer only exists inside an extends clause type Bad<T> = infer R; // Error: 'infer' declarations are only permitted in the 'extends' clause

infer 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

typescript
type Foo<T> = T extends { x: infer U } ? U : U; // Error - U is not in scope here

U 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

typescript
type ElementType<T> = T extends (infer U)[] ? U : never; type Result = ElementType<string[] | number[]>; // string | number

TypeScript 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

typescript
// 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>, and Awaited<T> are all built on infer in 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

typescript
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>; // never

The 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

typescript
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

typescript
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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?