Advanced Conditional Types With infer in TypeScript
Go beyond basic conditional types with infer, recursive conditionals, and distributive patterns to build powerful type-level programs.
What you'll learn
- ✓Use infer to extract types from complex structures
- ✓Build recursive conditional types for deep transformations
- ✓Control distributive behavior with wrapping tricks
Prerequisites
- •Basic TypeScript generics
- •Familiarity with union and intersection types
Conditional types are TypeScript’s if-else at the type level. The basic form T extends U ? X : Y is straightforward, but the real power comes from infer, recursion, and distribution control. This article covers the advanced patterns that let you build type-level parsers, transformers, and validators.
Quick Review: The Basics
A conditional type checks if a type extends another.
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
When T is a union, the conditional distributes over each member.
type C = IsString<'hello' | 42>; // true | false => boolean
The infer Keyword
The infer keyword declares a type variable inside a conditional type’s extends clause. TypeScript infers its value from the matched structure.
// Extract the return type of a function
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
type A = ReturnOf<() => string>; // string
type B = ReturnOf<(x: number) => boolean>; // boolean
You can infer from any structural position: array elements, promise values, object properties, template literals.
// Extract the element type of an array
type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
type C = ElementOf<string[]>; // string
type D = ElementOf<[number, string]>; // number | string
// Unwrap a promise
type Awaited<T> = T extends Promise<infer V> ? Awaited<V> : T;
type E = Awaited<Promise<Promise<string>>>; // string
Multiple infer Positions
You can use infer in multiple positions within the same conditional.
type FirstAndLast<T extends readonly unknown[]> =
T extends readonly [infer First, ...unknown[], infer Last]
? [First, Last]
: T extends readonly [infer Only]
? [Only, Only]
: never;
type A = FirstAndLast<[1, 2, 3, 4]>; // [1, 4]
type B = FirstAndLast<['only']>; // ['only', 'only']
You can also infer from function parameters.
type SecondParam<T> =
T extends (a: any, b: infer B, ...rest: any[]) => any ? B : never;
type P = SecondParam<(x: string, y: number, z: boolean) => void>;
// number
Recursive Conditional Types
TypeScript 4.1 added support for recursive conditional types. This lets you process arbitrarily deep structures.
// Deep readonly: makes every nested property readonly
type DeepReadonly<T> =
T extends Function
? T
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
interface Nested {
a: {
b: {
c: string;
};
d: number[];
};
}
type ReadonlyNested = DeepReadonly<Nested>;
// All properties at every level are readonly
Here is a type that flattens nested arrays.
type Flatten<T> = T extends readonly (infer E)[]
? Flatten<E>
: T;
type A = Flatten<number[][][]>; // number
type B = Flatten<string[]>; // string
type C = Flatten<boolean>; // boolean
Building a Type-Level Parser
Combining infer with template literal types lets you parse strings at the type level.
// Parse a comma-separated string into a tuple
type ParseCSV<S extends string> =
S extends `${infer Head},${infer Tail}`
? [Head, ...ParseCSV<Tail>]
: [S];
type Parsed = ParseCSV<'a,b,c,d'>;
// ['a', 'b', 'c', 'd']
// Parse key-value pairs from a query string
type ParseQuery<S extends string> =
S extends `${infer Key}=${infer Value}&${infer Rest}`
? { [K in Key]: Value } & ParseQuery<Rest>
: S extends `${infer Key}=${infer Value}`
? { [K in Key]: Value }
: {};
type Query = ParseQuery<'name=alice&age=30&role=admin'>;
// { name: 'alice' } & { age: '30' } & { role: 'admin' }
Controlling Distribution
By default, conditional types distribute over unions when the checked type is a naked type parameter. Sometimes you want to prevent this.
// Distributive: checks each union member separately
type IsStringDist<T> = T extends string ? 'yes' : 'no';
type A = IsStringDist<string | number>; // 'yes' | 'no'
// Non-distributive: wrapping in a tuple prevents distribution
type IsStringAll<T> = [T] extends [string] ? 'yes' : 'no';
type B = IsStringAll<string | number>; // 'no'
The wrapping trick with [T] extends [U] is essential when you need to check a union as a whole rather than member by member.
// Check if a type is exactly never
type IsNever<T> = [T] extends [never] ? true : false;
type C = IsNever<never>; // true
type D = IsNever<string>; // false
type E = IsNever<string | never>; // false
Inferring in Constrained Positions
You can add constraints to infer declarations since TypeScript 4.7.
type GetStringValues<T> =
T extends Record<string, infer V extends string> ? V : never;
type A = GetStringValues<{ a: 'hello'; b: 'world' }>;
// 'hello' | 'world'
// Without the constraint, V would include non-string values
type GetValues<T> =
T extends Record<string, infer V> ? V : never;
type B = GetValues<{ a: 'hello'; b: 42 }>;
// 'hello' | 42
Practical Example: Type-Safe API Response Handler
type ApiResponse<T> =
| { status: 'success'; data: T }
| { status: 'error'; message: string };
type ExtractData<T> =
T extends ApiResponse<infer D> ? D : never;
type UserResponse = ApiResponse<{ id: string; name: string }>;
type UserData = ExtractData<UserResponse>;
// { id: string; name: string }
// Build a handler map from a response union
type HandlerMap<T extends { status: string }> = {
[S in T['status']]: (
response: Extract<T, { status: S }>
) => void;
};
function handleResponse<T extends { id: string; name: string }>(
response: ApiResponse<T>,
handlers: HandlerMap<ApiResponse<T>>
) {
handlers[response.status](response as any);
}
Type-Level Arithmetic (Tuple Counting)
You can use tuple types and recursion for type-level counting.
type Length<T extends readonly unknown[]> = T['length'];
type BuildTuple<N extends number, T extends unknown[] = []> =
T['length'] extends N ? T : BuildTuple<N, [...T, unknown]>;
type Add<A extends number, B extends number> =
Length<[...BuildTuple<A>, ...BuildTuple<B>]>;
type Sum = Add<3, 4>; // 7
This is a niche technique, but it demonstrates the computational power of TypeScript’s type system.
Recursion Depth Limits
TypeScript limits recursive type instantiation to about 1000 levels. If your recursive conditional type hits this limit, restructure it. Common techniques include tail-call optimization patterns with accumulator type parameters or breaking the recursion into smaller chunks.
// Tail-call style with accumulator
type Reverse<T extends unknown[], Acc extends unknown[] = []> =
T extends [infer Head, ...infer Tail]
? Reverse<Tail, [Head, ...Acc]>
: Acc;
type Reversed = Reverse<[1, 2, 3, 4]>; // [4, 3, 2, 1]
Wrap Up
Advanced conditional types with infer let you extract, transform, and compute types from complex structures. Use recursive conditionals for deep transformations, control distribution with tuple wrapping, and constrain infer variables for precision. These patterns are the building blocks of type-level programming in TypeScript.
Related articles
- TypeScript TypeScript Generics: Advanced Patterns and Constraints
Master advanced TypeScript generics patterns including conditional constraints, recursive types, generic factories, and type-safe builder patterns with examples.
- TypeScript TypeScript Template Literal Types for String Manipulation
Master template literal types to build type-safe string patterns for routes, events, CSS values, and API endpoints in TypeScript.
- TypeScript TypeScript Generics Deep Dive
Go beyond basic generics — constraints, conditional types, mapped types, infer, and real-world patterns for writing type-safe reusable code.
- TypeScript TypeScript Conditional Types Tutorial
Learn conditional types in TypeScript: distribution, infer, and how to build expressive utility types that adapt to the input shape.