Skip to content

Infer and Type Extraction

advanced11 min read

Pattern Matching for Types

If you've ever used regex capture groups, you already know the mental model for infer. The infer keyword lets you extract parts of a type inside a conditional type. You describe a shape, mark the parts you want to capture, and TypeScript binds them to type variables. This is the mechanism behind ReturnType, Parameters, Awaited, and every advanced type utility that decomposes types.

Mental Model

Think of infer as a template with blanks. When you write T extends Promise<infer U>, you're saying: "If T matches the shape Promise<___>, capture whatever fills the blank and call it U." It's like regex capture groups but for types — you describe the pattern, mark the groups, and TypeScript fills them in from the actual type.

Basic infer Patterns

// Extract the return type of a function
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type A = MyReturnType<() => string>;         // string
type B = MyReturnType<(x: number) => boolean>; // boolean
type C = MyReturnType<string>;               // never (string isn't a function)

// Extract function parameters as a tuple
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;

type D = MyParameters<(a: string, b: number) => void>; // [a: string, b: number]

// Extract the resolved type of a Promise
type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T;

type E = MyAwaited<Promise<string>>;              // string
type F = MyAwaited<Promise<Promise<number>>>;     // number (recursive unwrapping)
type G = MyAwaited<string>;                       // string (not a promise — returns as-is)

Multiple infer Positions

Here's where it starts to get fun — you can use infer multiple times in the same pattern:

// Extract the first and rest of a tuple
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type Rest<T extends any[]> = T extends [any, ...infer R] ? R : never;

type H = First<[string, number, boolean]>; // string
type I = Rest<[string, number, boolean]>;  // [number, boolean]

// Extract key and value from an entry tuple
type EntryKey<T> = T extends [infer K, any] ? K : never;
type EntryValue<T> = T extends [any, infer V] ? V : never;

type J = EntryKey<["name", string]>;   // "name"
type K = EntryValue<["name", string]>; // string

// Extract the instance type from a constructor
type MyInstanceType<T> = T extends new (...args: any[]) => infer I ? I : never;

type L = MyInstanceType<typeof Map>; // Map<any, any>

infer with Constraints (TypeScript 4.7+)

This one is underrated. You can constrain what infer captures, making your patterns more precise:

// Only extract if the return type is a string
type StringReturn<T> = T extends (...args: any[]) => infer R extends string ? R : never;

type M = StringReturn<() => "hello">;  // "hello"
type N = StringReturn<() => number>;   // never (number doesn't extend string)

// Extract numeric keys from an object
type NumericKeys<T> = keyof {
  [K in keyof T as T[K] extends number ? K : never]: T[K];
};

// Or using infer with constraint
type ExtractNumbers<T> = {
  [K in keyof T as T[K] extends infer V extends number ? K : never]: V;
};
How infer resolution works with multiple candidates

When infer appears in a covariant position (like a return type), TypeScript infers the union of all candidates:

type CovariantInfer<T> = T extends { a: infer U; b: infer U } ? U : never;
type P = CovariantInfer<{ a: string; b: number }>; // string | number

// When infer is in a contravariant position (like a function parameter),
// TypeScript infers the INTERSECTION:
type ContravariantInfer<T> = T extends {
  a: (x: infer U) => void;
  b: (x: infer U) => void;
} ? U : never;
type Q = ContravariantInfer<{ a: (x: string) => void; b: (x: number) => void }>; // string & number = never

This variance behavior matters when building utility types that extract from multiple positions.

Common Trap

infer only works inside conditional types (T extends ... ? ... : ...). You can't use it in mapped types, interfaces, or standalone type aliases:

// ERROR — infer outside conditional type
type Bad = Promise<infer T>;

// CORRECT — infer inside conditional
type Good<T> = T extends Promise<infer U> ? U : never;

Production Scenario: Type-Safe API Client

// Define API routes as a type map
interface ApiRoutes {
  "GET /users": { response: User[]; params: { page: number } };
  "GET /users/:id": { response: User; params: { id: string } };
  "POST /users": { response: User; body: CreateUserDto };
  "PUT /users/:id": { response: User; params: { id: string }; body: UpdateUserDto };
  "DELETE /users/:id": { response: void; params: { id: string } };
}

// Extract method and path from the route key
type ExtractMethod<T extends string> = T extends `${infer M} ${string}` ? M : never;
type ExtractPath<T extends string> = T extends `${string} ${infer P}` ? P : never;

// Extract route params from path pattern
type ExtractRouteParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param]: string } & ExtractRouteParams<Rest>
    : T extends `${string}:${infer Param}`
      ? { [K in Param]: string }
      : {};

// Type-safe API client
type ApiClient = {
  [Route in keyof ApiRoutes as ExtractMethod<Route & string>]: <R extends Route>(
    path: ExtractPath<R & string>,
    options: Omit<ApiRoutes[R], "response">
  ) => Promise<ApiRoutes[R]["response"]>;
};

// Usage would give full autocomplete on paths, params, and response types
Execution Trace
Pattern
T extends `${infer M} ${string}`
'GET /users' matches — M captures 'GET'
Method
ExtractMethod<'GET /users'> = 'GET'
Template literal pattern extracts the HTTP method
Path
ExtractPath<'GET /users/:id'> = '/users/:id'
Second infer captures everything after the space
Params
ExtractRouteParams<'/users/:id'>
Recursive pattern extracts { id: string } from :id
Client
Full type-safe API client built from route definitions
Every endpoint gets correct params, body, and response types
What developers doWhat they should do
Using infer outside of conditional types
infer is a pattern-matching mechanism that needs the conditional type's structure to function
infer only works in the extends clause of conditional types: T extends ... infer U ... ? ... : ...
Forgetting that infer in covariant positions produces unions
TypeScript collects all candidates and unions them (covariant) or intersects them (contravariant)
When the same infer variable appears in multiple covariant positions, the result is a union of all candidates
Not handling the 'else' branch of conditional types with infer
If T doesn't match the pattern, the else branch is taken. never is the standard fallback for 'no match'.
Always provide a meaningful fallback: ... ? U : never (or a default type)
Over-complex infer patterns when a simpler indexed access works
If you just need a property type, T['propName'] is simpler than T extends { propName: infer U } ? U : never
Use T['key'] for simple property access — infer is for structural decomposition
Quiz
What does type R = MyAwaited<Promise<Promise<string>>> evaluate to?
Quiz
Given: type First<T> = T extends [infer F, ...any[]] ? F : never — what is First<[]>?
Quiz
Why does infer in a contravariant position (function parameter) produce an intersection instead of a union?

Challenge: Build a DeepAwaited Type

// Build DeepAwaited<T> that:
// 1. Unwraps Promise<T> recursively (like Awaited)
// 2. Also unwraps promises inside object properties
// 3. Also unwraps promises inside arrays
//
// type Input = {
//   user: Promise<{ name: string; posts: Promise<string[]> }>;
//   count: Promise<number>;
//   tags: string[];
// };
//
// type Output = DeepAwaited<Input>;
// Should be:
// {
//   user: { name: string; posts: string[] };
//   count: number;
//   tags: string[];
// }

// Your type here:
type DeepAwaited<T> =
  T extends Promise<infer U>
    ? DeepAwaited<U>
    : T extends Array<infer E>
      ? Array<DeepAwaited<E>>
      : T extends object
        ? { [K in keyof T]: DeepAwaited<T[K]> }
        : T;

type Output = DeepAwaited<Input>;
// {
//   user: { name: string; posts: string[] };
//   count: number;
//   tags: string[];
// }

The type checks in order: (1) If it's a Promise, unwrap and recurse. (2) If it's an array, recurse into elements. (3) If it's an object, recurse into each property. (4) Otherwise (primitives), return as-is. The ordering matters — Promise must be checked before object since Promise is an object.

Key Rules
  1. 1infer captures parts of a type inside conditional types — it's pattern matching for the type system
  2. 2infer only works inside conditional type extends clauses — not in mapped types or standalone
  3. 3Multiple infer in covariant positions produce unions; in contravariant positions produce intersections
  4. 4Recursive infer patterns (like Awaited) need a base case where T doesn't match the pattern
  5. 5Use indexed access (T['key']) for simple lookups — reserve infer for structural decomposition
1/10