TypeScript infer Keyword: Type Extraction in Conditionals
TypeScript's conditional types let you create types that branch based on type relationships. The basic syntax `T extends U ? X : Y` works well for simple checks, but what if you need to extract a...
Key Insights
- The
inferkeyword enables pattern matching on types within conditional types, allowing you to extract and capture type information from complex generic structures - Use
inferto build powerful utility types that unwrap Promises, extract function parameters and return types, and manipulate tuple types—capabilities that are impossible with basic generics alone - Multiple
inferdeclarations in a single conditional type enable sophisticated type transformations like tuple destructuring and recursive type extraction, but watch for distribution behavior with union types
Understanding the Problem: Why We Need Type Extraction
TypeScript’s conditional types let you create types that branch based on type relationships. The basic syntax T extends U ? X : Y works well for simple checks, but what if you need to extract a piece of type information from within a generic type?
Consider trying to get the return type of a function without infer:
// We want to extract the return type, but how?
type GetReturnType<T> = T extends (...args: any[]) => any ? /* what goes here? */ : never;
// We can check IF it's a function, but we can't capture the return type
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;
type Result = IsFunction<() => string>; // true, but we lost the string type
This is where infer comes in. It declares a type variable within the conditional’s extends clause, capturing type information for use in the true branch.
The infer Keyword: Syntax and Mechanics
The infer keyword follows this pattern:
T extends SomePattern<infer U> ? U : DefaultType
Here, U is a type variable that TypeScript will infer based on what matches that position in the pattern. If T matches the pattern, TypeScript extracts the type and makes it available as U in the true branch.
Let’s solve our return type problem:
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type StringReturn = GetReturnType<() => string>; // string
type NumberReturn = GetReturnType<(x: number) => number>; // number
type NeverReturn = GetReturnType<string>; // never (not a function)
Extracting array element types works similarly:
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type Num = ArrayElement<number[]>; // number
type Str = ArrayElement<string[]>; // string
type Multi = ArrayElement<(string | number)[]>; // string | number
The beauty of infer is that TypeScript does the hard work of figuring out what type fits in that position.
Common Patterns: Solving Real-World Type Problems
Unwrapping Promises
One of the most common uses of infer is extracting the resolved type from a Promise:
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type A = Unwrap<Promise<string>>; // string
type B = Unwrap<Promise<number>>; // number
type C = Unwrap<string>; // string (already unwrapped)
For nested Promises, you can make it recursive:
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
type Nested = DeepUnwrap<Promise<Promise<Promise<string>>>>; // string
Extracting Function Parameter Types
Getting all parameters as a tuple:
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function greet(name: string, age: number) {
return `${name} is ${age}`;
}
type GreetParams = Parameters<typeof greet>; // [string, number]
Getting just the first parameter:
type FirstParameter<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FirstParam = FirstParameter<typeof greet>; // string
Extracting Object Property Types
You can use infer with mapped types to extract property types:
type PropertyType<T, K extends keyof T> = T extends { [key in K]: infer V } ? V : never;
interface User {
name: string;
age: number;
active: boolean;
}
type NameType = PropertyType<User, 'name'>; // string
type AgeType = PropertyType<User, 'age'>; // number
Flattening Nested Arrays
type Flatten<T> = T extends (infer E)[] ? Flatten<E> : T;
type Deep = number[][][];
type Flat = Flatten<Deep>; // number
Advanced Patterns: Multiple infer Declarations
You can use multiple infer keywords in a single conditional type to extract multiple pieces of type information simultaneously.
Tuple Destructuring
Extract the first element and the rest:
type Head<T> = T extends [infer First, ...any[]] ? First : never;
type Tail<T> = T extends [any, ...infer Rest] ? Rest : never;
type Tuple = [string, number, boolean];
type First = Head<Tuple>; // string
type Rest = Tail<Tuple>; // [number, boolean]
Function Signature Decomposition
Extract both parameters and return type:
type FunctionParts<T> = T extends (...args: infer P) => infer R
? { params: P; return: R }
: never;
type Parts = FunctionParts<(x: string, y: number) => boolean>;
// { params: [string, number]; return: boolean }
Recursive Type Extraction
Build a type that reverses a tuple:
type Reverse<T extends any[]> = T extends [infer First, ...infer Rest]
? [...Reverse<Rest>, First]
: [];
type Original = [1, 2, 3, 4];
type Reversed = Reverse<Original>; // [4, 3, 2, 1]
Building Utility Types with infer
Let’s implement some of TypeScript’s built-in utility types to understand how they work.
Custom ReturnType
type MyReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : never;
function createUser(name: string) {
return { name, id: Math.random() };
}
type User = MyReturnType<typeof createUser>; // { name: string; id: number }
Custom Awaited
type MyAwaited<T> = T extends Promise<infer U>
? U extends Promise<any>
? MyAwaited<U>
: U
: T;
type A = MyAwaited<Promise<string>>; // string
type B = MyAwaited<Promise<Promise<number>>>; // number
ConstructorParameters for Classes
type MyConstructorParameters<T extends abstract new (...args: any) => any> =
T extends abstract new (...args: infer P) => any ? P : never;
class Database {
constructor(public host: string, public port: number) {}
}
type DbParams = MyConstructorParameters<typeof Database>; // [string, number]
Pitfalls and Best Practices
Union Type Distribution
Conditional types distribute over unions, which affects infer:
type UnwrapArray<T> = T extends Array<infer U> ? U : T;
type Result = UnwrapArray<string[] | number[]>; // string | number
// This distributes as:
// UnwrapArray<string[]> | UnwrapArray<number[]>
// which becomes: string | number
To prevent distribution, wrap the type in a tuple:
type NoDistribute<T> = [T] extends [Array<infer U>] ? U : T;
type Result2 = NoDistribute<string[] | number[]>; // string | number (still distributes in the infer)
Inference in Covariant Positions
TypeScript infers to the union of all candidates when infer appears multiple times in covariant positions:
type Union<T> = T extends { a: infer U; b: infer U } ? U : never;
type Result = Union<{ a: string; b: number }>; // string | number
In contravariant positions (function parameters), it infers to the intersection:
type Intersect<T> = T extends {
a: (x: infer U) => void;
b: (x: infer U) => void;
} ? U : never;
type Result = Intersect<{
a: (x: string) => void;
b: (x: number) => void;
}>; // string & number (which is never)
Performance Considerations
Deeply recursive types with infer can slow down type checking. TypeScript has recursion limits (typically 50 levels). For complex scenarios, consider:
// Instead of unbounded recursion
type DeepFlatten<T> = T extends (infer E)[] ? DeepFlatten<E> : T;
// Use a depth limit
type DeepFlattenLimited<T, Depth extends number = 5> = Depth extends 0
? T
: T extends (infer E)[]
? DeepFlattenLimited<E, [-1, 0, 1, 2, 3, 4][Depth]>
: T;
Conclusion
The infer keyword unlocks TypeScript’s type-level programming capabilities. It transforms conditional types from simple type guards into powerful pattern-matching tools that can extract, transform, and manipulate types in sophisticated ways.
Use infer when you need to capture type information from generic types—whether that’s unwrapping Promises, extracting function signatures, manipulating tuples, or building custom utility types. The patterns shown here form the foundation of advanced TypeScript libraries and enable the kind of type safety that makes TypeScript invaluable for large-scale applications.
Start with simple extractions like return types and array elements, then progress to multiple infer declarations and recursive patterns as your needs grow. Just remember to watch for union distribution behavior and keep recursion depth reasonable for compilation performance.