EdgeCases Logo
Feb 2026
TypeScript
Expert
8 min read

TypeScript infer: Advanced Extraction Patterns

Master TypeScript's infer keyword for nested type extraction, variance-aware inference, and recursive unwrapping patterns.

typescript
infer
conditional-types
type-extraction
generics
advanced

TypeScript's infer keyword extracts types from complex structures—function return types, promise results, array elements. But its real power emerges in advanced patterns: nested inference, multiple infers, variance positions, and recursive unwrapping.

The Basics: Single Inference

// Extract return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// Extract array element type
type ElementType<T> = T extends (infer E)[] ? E : never;

// Extract promise result
type Awaited<T> = T extends Promise<infer R> ? R : T;

infer declares a type variable within a conditional type's extends clause. If the pattern matches, the inferred type is captured and available in the true branch.

Multiple infer Declarations

You can use multiple infer keywords in a single conditional type:

// Extract both parameter and return types
type FunctionParts<T> = T extends (arg: infer A) => infer R
  ? { arg: A; return: R }
  : never;

type Parts = FunctionParts<(x: string) => number>;
// { arg: string; return: number }

// Extract key and value from Map
type MapTypes<T> = T extends Map<infer K, infer V>
  ? { key: K; value: V }
  : never;

type M = MapTypes<Map<string, number>>;
// { key: string; value: number }

Nested Inference

When working with deeply nested types—common in API responses or codegen—stack infer patterns:

type APIResponse = {
  data: {
    users: Array<{
      profile: {
        name: string;
        email: string;
      };
    }>;
  };
};

// Extract the profile type
type ExtractProfile<T> = T extends {
  data: {
    users: Array<{
      profile: infer P;
    }>;
  };
}
  ? P
  : never;

type Profile = ExtractProfile<APIResponse>;
// { name: string; email: string }

This is powerful for extracting types from third-party schemas without manually duplicating their structure.

Recursive Inference

Use recursion to unwrap nested wrappers like Promise or Array:

// Deeply unwrap promises
type DeepAwaited<T> = T extends Promise<infer R>
  ? DeepAwaited<R>
  : T;

type Nested = Promise<Promise<Promise<string>>>;
type Result = DeepAwaited<Nested>; // string

// Flatten nested arrays
type DeepFlatten<T> = T extends (infer E)[]
  ? DeepFlatten<E>
  : T;

type NestedArray = number[][][];
type Flat = DeepFlatten<NestedArray>; // number

Covariant vs Contravariant Positions

Where you place infer affects how TypeScript resolves multiple candidates. This is where things get subtle:

// Covariant position (return type): union of candidates
type CovariantInfer<T> = T extends { a: infer U; b: infer U } ? U : never;

type Covariant = CovariantInfer<{ a: string; b: number }>;
// string | number (union)

// Contravariant position (parameter): intersection of candidates
type ContravariantInfer<T> = T extends {
  a: (x: infer U) => void;
  b: (x: infer U) => void;
} ? U : never;

type Contravariant = ContravariantInfer<{
  a: (x: string) => void;
  b: (x: number) => void;
}>;
// string & number = never (intersection)

Rule: Same infer U in covariant positions → union. Same infer U in contravariant positions → intersection.

Practical Pattern: Extract Event Payload

type Events = {
  userCreated: { userId: string; timestamp: number };
  orderPlaced: { orderId: string; items: string[] };
  paymentFailed: { error: string; amount: number };
};

type EventPayload<K extends keyof Events> = Events[K];

// Type-safe event handler
function on<K extends keyof Events>(
  event: K,
  handler: (payload: EventPayload<K>) => void
) { /* ... */ }

on('userCreated', (payload) => {
  // payload is { userId: string; timestamp: number }
});

Now combine with infer for more dynamic extraction:

// Extract all payload types as union
type AllPayloads<T> = T extends Record<string, infer P> ? P : never;

type AnyPayload = AllPayloads<Events>;
// { userId: string; timestamp: number } | { orderId: string; items: string[] } | ...

Template Literal Inference

infer works with template literal types to parse strings:

// Parse "key:value" strings
type ParseKV<S extends string> = S extends `${infer K}:${infer V}`
  ? { key: K; value: V }
  : never;

type Parsed = ParseKV<'name:John'>;
// { key: 'name'; value: 'John' }

// Extract route params
type ExtractParams<Path extends string> =
  Path extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<`/${Rest}`>
    : Path extends `${string}:${infer Param}`
      ? Param
      : never;

type Params = ExtractParams<'/users/:userId/posts/:postId'>;
// 'userId' | 'postId'

Constrained Inference with extends

Add constraints to inferred types using infer X extends Type:

// Only infer if it's a string
type StringKeys<T> = T extends Record<infer K extends string, any>
  ? K
  : never;

type Keys = StringKeys<{ a: 1; b: 2; [Symbol.iterator]: 3 }>;
// 'a' | 'b' (symbol excluded)

// Infer array of specific type
type NumberArrayElement<T> = T extends (infer E extends number)[]
  ? E
  : never;

type N = NumberArrayElement<[1, 2, 3]>; // 1 | 2 | 3
type S = NumberArrayElement<string[]>; // never

Edge Cases

Distributive Behavior

Conditional types with naked type parameters distribute over unions:

type ToArray<T> = T extends any ? T[] : never;

type Distributed = ToArray<string | number>;
// string[] | number[] (distributed)

// Prevent distribution with tuple wrapper
type NoDistribute<T> = [T] extends [any] ? T[] : never;

type NonDistributed = NoDistribute<string | number>;
// (string | number)[]

Inference Depth Limits

TypeScript has recursion limits. Deeply nested inference can hit them:

// May hit "Type instantiation is excessively deep" at ~50 levels
type DeepUnwrap<T> = T extends { inner: infer U } ? DeepUnwrap<U> : T;

For very deep structures, consider runtime validation or code generation.

Key Takeaways

  • infer extracts types from patterns—use it to avoid manual type duplication
  • Multiple infer declarations in one conditional capture multiple parts
  • Covariant positions (return types) produce unions; contravariant (parameters) produce intersections
  • Use infer X extends Type to constrain what can be inferred
  • Recursive infer patterns can deeply unwrap nested structures
  • Watch for distribution with unions and recursion depth limits

Advertisement

Related Insights

Explore related edge cases and patterns

TypeScript
Expert
TypeScript Mapped Type Modifiers: When Inference Breaks
7 min
TypeScript
Expert
TypeScript Distributive Conditional Types: The Union Distribution Rule
8 min

Advertisement