TypeScript infer: Advanced Extraction Patterns
Master TypeScript's infer keyword for nested type extraction, variance-aware inference, and recursive unwrapping patterns.
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>; // numberCovariant 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[]>; // neverEdge 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
inferextracts types from patterns—use it to avoid manual type duplication- Multiple
inferdeclarations in one conditional capture multiple parts - Covariant positions (return types) produce unions; contravariant (parameters) produce intersections
- Use
infer X extends Typeto constrain what can be inferred - Recursive
inferpatterns can deeply unwrap nested structures - Watch for distribution with unions and recursion depth limits
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Tools & Utilities
Related Insights
Explore related edge cases and patterns
Advertisement