TypeScript 32 🔷 Infer Keyword and Conditional Type Inference
infer is the keyword that turns a conditional type from a test into an extraction. Without it, T extends U ? X : Y only answers “does T match U?” — yes or no. With infer, you can capture a part of T and use it in the true branch. Every utility type that pulls something out of another type — ReturnType, Parameters, Awaited, InstanceType — is a conditional type with infer. This chapter is about that keyword: where it can appear, what it can capture, how it interacts with distribution, and how to constrain it.
Key point: infer X declares a type variable inside the pattern on the left of extends. If the pattern matches, X is bound to whatever type fits that position. infer only appears in the true branch’s condition — never in the false branch, never outside a conditional. You can infer in many positions: function parameters, return types, array elements, promise values, tuple slots, object properties. Constrain the capture with infer X extends Y when you need to reject non-matching cases.
What infer does
infer declares a type variable that captures part of a pattern.
type ElementType<T> = T extends (infer U)[] ? U : never;
type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
type C = ElementType<boolean>; // never
Reading T extends (infer U)[]:
- Match
Tagainst the pattern “an array of something” - Bind the “something” to
U - In the true branch,
Uis available
For string[], U is string. For number[], U is number. For boolean (not an array), the match fails and the false branch returns never.
Where infer goes: Inside the pattern on the left of extends, in a position where a type is expected.
T extends Promise<infer U> ? U : never
T extends (infer U)[] ? U : never
T extends (...args: infer P) => any ? P : never
T extends (...args: any) => infer R ? R : never
Each captures a different slot.
Where infer doesn’t go: In the false branch.
// ❌ invalid
type Bad<T> = T extends string ? 'yes' : infer R;
The false branch isn’t a pattern — there’s nothing to match, so nothing to capture.
What infer can capture:
- Any type at that position
- Nothing (if the pattern doesn’t match)
What infer can’t do:
- Capture a value (only types)
- Appear in a value position
- Be reused outside the conditional’s branches
- Escape the scope of the conditional
Why infer matters: It’s the difference between “this type is an array” and “this type is an array of strings.” The first is a test; the second is an extraction. infer gives you the extracted piece to use downstream. Without it, TypeScript’s utility types couldn’t exist.
Why the name “infer”: The type isn’t given — it’s inferred from the shape of the input. You’re telling TypeScript to figure out what fits that position. The name matches how the compiler works: it uses the input type to deduce the variable’s type.
infer in different positions
infer works in almost any type pattern.
Array element:
type Element<T> = T extends (infer U)[] ? U : never;
type A = Element<string[]>; // string
type B = Element<number[]>; // number
Readonly array element:
type Element2<T> = T extends readonly (infer U)[] ? U : never;
type A = Element2<readonly string[]>; // string
type B = Element2<string[]>; // string
Use readonly (infer U)[] to match both mutable and readonly arrays.
Promise value:
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 (not a promise)
Function return type:
type Return<T> = T extends (...args: any[]) => infer R ? R : never;
type A = Return<() => string>; // string
type B = Return<(x: number) => boolean>; // boolean
Function parameter tuple:
type Params<T> = T extends (...args: infer P) => any ? P : never;
type A = Params<(x: number, y: string) => void>;
// [x: number, y: string]
The parameters become a labeled tuple.
Object property:
type Prop<T, K extends keyof T> = T[K];
That’s not infer — indexed access. For infer on an object pattern:
type IdOf<T> = T extends { id: infer I } ? I : never;
type A = IdOf<{ id: number; name: string }>; // number
type B = IdOf<{ id: string }>; // string
type C = IdOf<{ name: string }>; // never
infer I captures the type of the id property.
Record value:
type ValueOf<T> = T extends Record<string, infer V> ? V : never;
type A = ValueOf<{ a: number; b: number }>; // number
type B = ValueOf<{ a: string }>; // string
Note: Record<string, V> is { [key: string]: V } — matches any object with a string index signature. For a specific object, it matches to the union of its values only if the object is “record-like.”
Tuple positions:
type First<T> = T extends [infer F, ...unknown[]] ? F : never;
type Last<T> = T extends [...unknown[], infer L] ? L : never;
type A = First<[1, 2, 3]>; // 1
type B = Last<[1, 2, 3]>; // 3
Constructor instance:
type Instance<T> = T extends new (...args: any[]) => infer R ? R : never;
class User {}
type U = Instance<typeof User>; // User
Multiple infers in one pattern:
type Pair<T> = T extends [infer A, infer B] ? [B, A] : never;
type A = Pair<[string, number]>; // [number, string]
Each infer captures a different slot.
Why multiple positions matter: Every structure has parts. Function types have parameters and return types. Tuples have positions. Objects have properties. infer captures whichever parts the pattern names. That’s how utilities like Parameters and ReturnType extract different pieces of the same function type.
Why
inferis scoped to the true branch: The pattern on the left ofextendsis what determines the variable’s type. The variable only exists when the pattern matched — which is the true branch. In the false branch, there was no match, so no captured type. The scope reflects the semantics.
infer with constraints
TypeScript 4.7 added infer X extends Y — constraining what the variable can capture.
type FirstNumber<T> =
T extends [infer N extends number, ...unknown[]] ? N : never;
type A = FirstNumber<[1, 'a']>; // 1
type B = FirstNumber<['a', 1]>; // never
infer N extends number requires the captured type to be assignable to number. If the pattern doesn’t match with that constraint, the false branch runs.
Without the constraint:
type FirstAny<T> = T extends [infer F, ...unknown[]] ? F : never;
type A = FirstAny<[1, 'a']>; // 1
type B = FirstAny<['a', 1]>; // 'a'
No filtering — anything in the first position is captured.
With the constraint:
type FirstNumeric<T> =
T extends [infer N extends number, ...unknown[]] ? N : never;
type A = FirstNumeric<[1, 'a']>; // 1
type B = FirstNumeric<['a', 1]>; // never
'a' doesn’t extend number, so the match fails.
Constraining to string:
type OnlyStrings<T> =
T extends (infer S extends string)[] ? S[] : never;
type A = OnlyStrings<['a', 'b']>; // ('a' | 'b')[]
type B = OnlyStrings<[1, 2]>; // never
type C = OnlyStrings<['a', 1]>; // never — mixed
The whole array must be strings for the pattern to match.
Recursive with constraints:
type ExtractNumbers<T> =
T extends readonly [infer N extends number, ...infer Rest]
? [N, ...ExtractNumbers<Rest>]
: [];
type A = ExtractNumbers<[1, 'a', 2, 'b', 3]>;
// [1, 2, 3]
The recursion captures only numbers, skipping the rest.
When to constrain:
- When the inferred type must satisfy a requirement
- When you want to reject non-matching cases instead of capturing them
- When the constraint narrows what the result can be
When not to constrain:
- When you want to capture anything at that position
- When the constraint would make the pattern too specific
- When you’ll check the type later anyway
Why constraints exist: Without them, you’d capture a type and then check it in a nested conditional. infer X extends Y collapses both into one step. Fewer branches, clearer intent.
Why constraints were added late: The original
inferwas added in TypeScript 2.8. Constraints came in 4.7. Until then, extracting a constrained type required a two-step conditional: capture withinfer, then test the captured type. The constraint makes the common case one step. It’s sugar, but useful sugar.
Common infer patterns
Several patterns show up repeatedly.
Unwrap a Promise:
type Unwrap<T> = T extends Promise<infer U> ? U : T;
Deep unwrap:
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
type A = DeepUnwrap<Promise<Promise<number>>>; // number
Element of an array:
type ElementOf<T> = T extends (infer U)[] ? U : never;
type A = ElementOf<string[]>; // string
First parameter:
type FirstParam<T> =
T extends (first: infer F, ...rest: any[]) => any ? F : never;
type A = FirstParam<(name: string, age: number) => void>; // string
Last element of a tuple:
type Last<T> =
T extends [...unknown[], infer L] ? L : never;
type A = Last<[1, 2, 3]>; // 3
Reverse a tuple:
type Reverse<T> =
T extends [infer First, ...infer Rest]
? [...Reverse<Rest>, First]
: [];
type A = Reverse<[1, 2, 3]>; // [3, 2, 1]
Return type of async function:
type AsyncReturn<T> =
T extends (...args: any[]) => Promise<infer R> ? R : never;
type A = AsyncReturn<() => Promise<string>>; // string
Extract keys of a specific value type:
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
interface User { id: number; name: string; email: string; active: boolean; }
type NumberKeys = KeysOfType<User, number>; // 'id'
Why these patterns recur: Each extracts a common piece of a structure — the value of a promise, the element of an array, the return type of a function. They’re the same infer in different positions.
Why
inferwith recursion is powerful:DeepUnwrapandReverseshow thatinfercomposes with itself. Each recursion step captures a piece and processes the rest. That’s how type-level algorithms — flattening, reversing, filtering — are written.
infer distribution
infer interacts with distribution. When the pattern is distributive, infer can capture different things per union member.
type UnwrapAll<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapAll<Promise<string> | Promise<number>>;
// string | number
The union of promises distributes: each member is unwrapped, and the results are combined.
What happens if some members don’t match:
type A = UnwrapAll<Promise<string> | number>;
// string | number
The Promise<string> matches and is unwrapped to string. The number doesn’t match and passes through. Result: string | number.
Capturing a union of different types:
type Values<T> = T extends { value: infer V } ? V : never;
type A = Values<{ value: string } | { value: number }>;
// string | number
Each object member is matched; its value type is captured; results combine.
Preventing distribution on infer:
type WholeUnwrap<T> = [T] extends [Promise<infer U>] ? U : never;
type A = WholeUnwrap<Promise<string> | Promise<number>>;
// never (the whole union isn't a Promise)
Wrapping in [ ] stops distribution. The union as a whole must be a Promise for the pattern to match.
Why distribution matters for infer: It lets you handle unions member by member. UnwrapAll<Promise<string> | Promise<number>> captures the value type of each. Without distribution, the pattern would fail because the union of promises isn’t itself a promise.
When to prevent distribution on infer:
- When you want to match the whole union, not each member
- When testing whether a union is a specific type
- When you need the captured type to be the union itself, not its members
Why distribution and
inferare intertwined: Both are about unions. Distribution splits a union into members;infercaptures per member. Combined, they give youMap-like operations over unions. Understanding both is understanding how TypeScript handles collections of types.
A full example
A library of type-level utilities using infer.
// ============================================
// UNWRAPPING
// ============================================
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type DeepUnwrap<T> =
T extends Promise<infer U>
? DeepUnwrap<U>
: T;
// ============================================
// EXTRACTING FROM FUNCTIONS
// ============================================
type MyReturnType<T> =
T extends (...args: any[]) => infer R ? R : never;
type MyParameters<T> =
T extends (...args: infer P) => any ? P : never;
type MyFirstParam<T> =
T extends (first: infer F, ...rest: any[]) => any ? F : never;
type AsyncReturnType<T> =
T extends (...args: any[]) => Promise<infer R> ? R : never;
// ============================================
// EXTRACTING FROM COLLECTIONS
// ============================================
type ElementOf<T> = T extends (infer U)[] ? U : never;
type FirstOf<T> =
T extends [infer F, ...unknown[]] ? F : never;
type LastOf<T> =
T extends [...unknown[], infer L] ? L : never;
type Reverse<T> =
T extends [infer F, ...infer R]
? [...Reverse<R>, F]
: [];
// ============================================
// EXTRACTING FROM OBJECTS
// ============================================
type PropType<T, K extends keyof T> = T[K];
type ValueOf<T> =
T extends Record<string, infer V> ? V : never;
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
// ============================================
// CONSTRAINED INFER
// ============================================
type NumericKeys<T> = {
[K in keyof T]: T[K] extends number ? K : never;
}[keyof T];
type FirstNumber<T> =
T extends [infer N extends number, ...unknown[]] ? N : never;
// ============================================
// APPLYING THEM
// ============================================
interface User {
id: number;
name: string;
email: string;
active: boolean;
}
// Function types
function createUser(name: string, age: number): User {
return { id: 1, name, email: `${name}@x.com`, active: true };
}
type CreateUserReturn = MyReturnType<typeof createUser>;
// User
type CreateUserParams = MyParameters<typeof createUser>;
// [name: string, age: number]
type FirstName = MyFirstParam<typeof createUser>;
// string
// Promise types
type UserPromise = Promise<User>;
type UnwrappedUser = Unwrap<UserPromise>;
// User
type NestedPromise = Promise<Promise<string>>;
type UnwrappedString = DeepUnwrap<NestedPromise>;
// string
// Array types
type Names = string[];
type Name = ElementOf<Names>;
// string
type Tuple = [1, 2, 3];
type First = FirstOf<Tuple>;
// 1
type Last = LastOf<Tuple>;
// 3
type Reversed = Reverse<Tuple>;
// [3, 2, 1]
// Object types
type UserId = User['id'];
// number
type NumberProps = KeysOfType<User, number>;
// 'id'
type Numeric = FirstNumber<[10, 'x', 20]>;
// 10
// ============================================
// USAGE
// ============================================
const user: CreateUserReturn = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
active: true
};
const params: CreateUserParams = ['Alice', 30];
console.log(user);
console.log(params);
What this shows:
UnwrapandDeepUnwrap— promise value, recursivelyMyReturnType,MyParameters,MyFirstParam— pieces of a function typeAsyncReturnType— unwrapping an async function’s resultElementOf,FirstOf,LastOf— array and tuple positionsReverse— recursive tuple manipulationValueOf,KeysOfType— object value typesFirstNumber— constrainedinfer
Every utility extracts a type via infer. Together they form a small type-level toolkit.
Why this shape: It’s how real type-level libraries look. Each utility is a few lines of
inferand conditional logic. Combining them produces more complex extractions. Once you can write these, you can read any library’s type helpers.
Complete Example Session
# ============================================
# PART 1: BASIC INFER
# ============================================
cat > basic.ts << 'EOF'
type ElementType<T> = T extends (infer U)[] ? U : never;
type A = ElementType<string[]>;
type B = ElementType<number[]>;
type C = ElementType<boolean>;
const a: A = 'hello';
const b: B = 42;
const c: C = undefined as never;
console.log(a, b, c);
EOF
npx tsc --noEmit basic.ts
# (no errors)
# ============================================
# PART 2: INFER IN MULTIPLE POSITIONS
# ============================================
cat > positions.ts << 'EOF'
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
type ParamsOf<T> = T extends (...args: infer P) => any ? P : never;
type FirstOf<T> = T extends [infer F, ...unknown[]] ? F : never;
type PropType<T> = T extends { value: infer V } ? V : never;
type A = UnwrapPromise<Promise<string>>;
type B = ReturnOf<() => number>;
type C = ParamsOf<(x: string, y: boolean) => void>;
type D = FirstOf<[1, 2, 3]>;
type E = PropType<{ value: Date }>;
const a: A = 'hello';
const b: B = 42;
const c: C = ['x', true];
const d: D = 1;
const e: E = new Date();
console.log(a, b, c, d, e);
EOF
npx tsc --noEmit positions.ts
# (no errors)
# ============================================
# PART 3: CONSTRAINED INFER
# ============================================
cat > constrained.ts << 'EOF'
type FirstNumber<T> =
T extends [infer N extends number, ...unknown[]] ? N : never;
type FirstString<T> =
T extends [infer S extends string, ...unknown[]] ? S : never;
type A = FirstNumber<[1, 'a']>;
type B = FirstNumber<['a', 1]>;
type C = FirstString<['hello', 1]>;
type D = FirstString<[1, 'hello']>;
const a: A = 1;
const b: B = undefined as never;
const c: C = 'hello';
const d: D = undefined as never;
console.log(a, b, c, d);
EOF
npx tsc --noEmit constrained.ts
# (no errors)
# ============================================
# PART 4: RECURSIVE INFER
# ============================================
cat > recursive.ts << 'EOF'
type Reverse<T> =
T extends [infer F, ...infer R]
? [...Reverse<R>, F]
: [];
type DeepUnwrap<T> =
T extends Promise<infer U>
? DeepUnwrap<U>
: T;
type A = Reverse<[1, 2, 3, 4]>;
type B = DeepUnwrap<Promise<Promise<string>>>;
const a: A = [4, 3, 2, 1];
const b: B = 'hello';
console.log(a, b);
EOF
npx tsc --noEmit recursive.ts
# (no errors)
# ============================================
# PART 5: DISTRIBUTION WITH INFER
# ============================================
cat > distribution.ts << 'EOF'
type UnwrapAll<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapAll<Promise<string> | Promise<number>>;
// string | number
type B = UnwrapAll<Promise<string> | number>;
// string | number
// Prevent distribution
type WholeUnwrap<T> = [T] extends [Promise<infer U>] ? U : never;
type C = WholeUnwrap<Promise<string> | Promise<number>>;
// never
const a: A = 'hello';
const b: B = 'hello';
const c: C = undefined as never;
console.log(a, b, c);
EOF
npx tsc --noEmit distribution.ts
# (no errors)
# ============================================
# PART 6: FULL UTILITY LIBRARY
# ============================================
cat > library.ts << 'EOF'
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type ElementOf<T> = T extends (infer U)[] ? U : never;
type FirstOf<T> = T extends [infer F, ...unknown[]] ? F : never;
type LastOf<T> = T extends [...unknown[], infer L] ? L : never;
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
interface Product {
id: number;
name: string;
price: number;
inStock: boolean;
}
function fetchProduct(id: number): Promise<Product> {
return Promise.resolve({ id, name: 'Widget', price: 9.99, inStock: true });
}
type FetchReturn = MyReturnType<typeof fetchProduct>;
// Promise<Product>
type ProductFromFetch = Unwrap<FetchReturn>;
// Product
type ProductNumberKeys = KeysOfType<Product, number>;
// 'id' | 'price'
const idKeys: ProductNumberKeys[] = ['id', 'price'];
console.log(idKeys);
EOF
npx tsc --noEmit library.ts
# (no errors)
# ============================================
# PART 7: COMPILE AND RUN
# ============================================
npx tsc basic.ts positions.ts constrained.ts recursive.ts distribution.ts library.ts
node basic.js
# [ hello 42 undefined ]
node positions.js
# [ hello 42 [ 'x', true ] 1 2025-09-22T... ]
node constrained.js
# [ 1 undefined hello undefined ]
node recursive.js
# [ [ 4, 3, 2, 1 ] hello ]
node distribution.js
# [ hello hello undefined ]
node library.js
# [ [ 'id', 'price' ] ]
Quick Reference
infer Syntax
| Form | Meaning |
|---|---|
infer X | Capture a type |
infer X extends Y | Capture with constraint |
infer X, infer Y | Multiple captures |
...infer Rest | Capture the rest |
Positions
| Pattern | Captures |
|---|---|
T extends (infer U)[] | Array element |
T extends readonly (infer U)[] | Readonly element |
T extends Promise<infer U> | Promise value |
T extends (...args: infer P) => any | Parameter tuple |
T extends (...args: any) => infer R | Return type |
T extends { x: infer X } | Property type |
T extends Record<string, infer V> | Value type |
T extends [infer F, ...unknown[]] | First element |
T extends [...unknown[], infer L] | Last element |
T extends [infer F, ...infer R] | First and rest |
T extends new (...args: any) => infer I | Instance type |
Standard Library
| Type | infer Pattern |
|---|---|
ReturnType<T> | (...a) => infer R |
Parameters<T> | (...a: infer P) => any |
Awaited<T> | Promise<infer U> |
InstanceType<T> | new (...a) => infer R |
Common Patterns
| Pattern | Purpose |
|---|---|
Unwrap<T> | T extends Promise<infer U> ? U : T |
ElementOf<T> | T extends (infer U)[] ? U : never |
First<T> | T extends [infer F, ...] ? F : never |
Last<T> | T extends [..., infer L] ? L : never |
DeepUnwrap<T> | Recursive promise unwrap |
Reverse<T> | Recursive tuple reverse |
Constrained Infer
| Form | Effect |
|---|---|
infer N extends number | Only matches numbers |
infer S extends string | Only matches strings |
infer F extends Function | Only matches functions |
infer A extends any[] | Only matches arrays |
Distribution Rules
| Left side | Distribution |
|---|---|
T (naked) | Distributes |
[T] | Doesn’t distribute |
| Concrete type | Doesn’t distribute |
Distribution with infer
| Input | Pattern | Result |
|---|---|---|
Promise<string> | Promise<number> | Promise<infer U> | string | number |
Promise<string> | number | Promise<infer U> | string | number |
[Promise<string> | Promise<number>] | [Promise<infer U>] | never |
Recursion
| Type | Base case |
|---|---|
DeepUnwrap<T> | Not a Promise → return T |
Reverse<T> | Empty tuple → return [] |
ExtractNumbers<T> | Empty tuple → return [] |
infer vs Indexed Access
| Approach | Use for |
|---|---|
T[K] | Property type when K is known |
infer | Extracting when pattern matching |
Error Cases
| Error | Cause | Fix |
|---|---|---|
infer not allowed | Wrong position | Move to true branch |
Cannot find name X | Used outside scope | Move into branch |
| Infinite recursion | No base case | Add terminal |
| Wrong capture | Pattern mismatch | Adjust pattern |
Utility Combinations
| Type | Result |
|---|---|
Unwrap<ReturnType<T>> | Async function’s value |
Parameters<typeof fn> | Function parameters |
Awaited<ReturnType<T>> | Same as above |
InstanceType<typeof C> | Class instance |
First<Reverse<T>> | Last element |
When to Use infer
| Situation | Use infer |
|---|---|
| Extract from structure | ✅ |
| Pattern match + extract | ✅ |
| Recursive extraction | ✅ |
| Simple property access | ❌ (use T[K]) |
| Whole union matching | ⚠️ wrap in [ ] |
Best Practices
✅ Do This:
// Use infer to extract a value type
type Unwrap<T> = T extends Promise<infer U> ? U : T; // ✅
// Extract function return
type Return<T> = T extends (...args: any[]) => infer R ? R : never; // ✅
// Use constraints when the type must match
type FirstNum<T> =
T extends [infer N extends number, ...unknown[]] ? N : never; // ✅
// Use readonly pattern for both array types
type Elem<T> = T extends readonly (infer U)[] ? U : never; // ✅
// Recurse with a base case
type Flat<T> = T extends (infer U)[] ? Flat<U> : T; // ✅
// Capture multiple positions
type Swap<T> = T extends [infer A, infer B] ? [B, A] : never; // ✅
// Combine with distribution for unions
type AnyUnwrap<T> = T extends Promise<infer U> ? U : T; // ✅
// Prevent distribution when matching whole union
type WholePromise<T> = [T] extends [Promise<infer U>] ? U : never; // ✅
// Use named types for readable recursion
type DeepReadonly<T> =
T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T; // ✅
❌ Don’t Do This:
// Don't use infer outside a conditional
type Bad<T> = infer U; // ❌ syntax error // ❌
// Don't use infer in the false branch
type Bad<T> = T extends string ? 'yes' : infer R; // ❌ // ❌
// Don't forget the pattern needs to match
type Bad<T> = T extends (infer U)[] ? U : never;
// Bad<string> = never, not string — check the else branch // ⚠️
// Don't recurse without a base case
type Loop<T> = T extends (infer U)[] ? Loop<U> : T;
// Terminates because non-arrays hit the else branch // ✅
// Don't distribute when matching whole unions
type Bad<T> = T extends Promise<infer U> ? U : never;
// Promise<string> | number → string, drops number // ⚠️
// Don't over-constrain infer
type Bad<T> = T extends [infer X extends string & number] ? X : never;
// Never matches // ⚠️
// Don't use infer when T[K] works
type Bad<T, K extends keyof T> = T extends { [P in K]: infer V } ? V : never;
// Just use T[K] // ⚠️
// Don't mix distribution and whole-union logic
type Bad<T> = [T] extends [Promise<infer U> | Promise<infer U>] ? U : never;
// Weird behavior // ⚠️
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
infer outside conditional | Syntax error | Move inside |
infer in false branch | Syntax error | Use true branch only |
| Distribution surprise | Wrong result for unions | Wrap in [ ] |
| Missing base case | Infinite recursion | Add terminal case |
| Over-constrained infer | Never matches | Loosen constraint |
Forgot readonly | Doesn’t match readonly arrays | Use readonly (infer U)[] |
| Wrong pattern shape | Never matches | Match the structure |
Confused with T[K] | Wrong tool | Use indexed access |
| Recursion too deep | Type complexity | Simplify |
Real-World Examples
1. Unwrap Promise
type Unwrap<T> = T extends Promise<infer U> ? U : T;
2. Deep unwrap
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
3. Element of array
type Element<T> = T extends (infer U)[] ? U : never;
4. Element of readonly array
type ElementRO<T> = T extends readonly (infer U)[] ? U : never;
5. Function return
type Return<T> = T extends (...args: any) => infer R ? R : never;
6. Function params
type Params<T> = T extends (...args: infer P) => any ? P : never;
7. First tuple element
type First<T> = T extends [infer F, ...unknown[]] ? F : never;
8. Last tuple element
type Last<T> = T extends [...unknown[], infer L] ? L : never;
9. Constructor instance
type Instance<T> = T extends new (...args: any) => infer R ? R : never;
10. Property type via infer
type PropType<T> = T extends { value: infer V } ? V : never;
11. Constrained first number
type FirstNum<T> =
T extends [infer N extends number, ...unknown[]] ? N : never;
12. Reverse tuple
type Reverse<T> =
T extends [infer F, ...infer R] ? [...Reverse<R>, F] : [];
13. Async function return
type AsyncReturn<T> =
T extends (...args: any[]) => Promise<infer R> ? R : never;
14. Keys by value type
type KeysOfType<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
15. Extract string array
type StringArray<T> =
T extends (infer S extends string)[] ? S[] : never;
16. Unwrap array of promises
type PromiseValues<T> =
T extends Promise<infer U>[] ? U[] : never;
17. First parameter
type FirstParam<T> =
T extends (first: infer F, ...rest: any[]) => any ? F : never;
18. Swap tuple
type Swap<T> = T extends [infer A, infer B] ? [B, A] : never;
19. Union to tuple
type UnionToTuple<T> =
LastOf<UnionToIntersection<T extends any ? () => T : never>>;
20. Flatten nested arrays
type Flat<T> = T extends (infer U)[] ? Flat<U> : T;
Visual: infer Flow
┌──────────────────────────────────────────────┐
│ type ElementType<T> = T extends (infer U)[] ? U : never│
│ │
└──────────────────────────────────────────────┘
│
│ called with string[]
▼
┌──────────────────────────────────────────────┐
│ string[] extends (infer U)[] │
│ │ │
│ ▼ │
│ match: yes │
│ U is captured as: string │
│ │ │
│ ▼ │
│ true branch: U → string │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ called with number │
│ │
│ number extends (infer U)[] │
│ │ │
│ ▼ │
│ match: no │
│ │ │
│ ▼ │
│ false branch: never │
│ │
└──────────────────────────────────────────────┘
Visual: Multiple Positions
┌──────────────────────────────────────────────┐
│ T extends (...args: infer P) => infer R │
│ │ │ │ │
│ │ │ │ │
│ │ │ └ return │
│ │ └ params │
│ └ the function │
│ │
│ T = (x: number, y: string) => boolean │
│ │
│ P = [x: number, y: string] │
│ R = boolean │
│ │
└──────────────────────────────────────────────┘
Visual: Constrained Infer
┌──────────────────────────────────────────────┐
│ T extends [infer N extends number, ...unknown[]]│
│ ─────┬────────── │
│ │ │
│ N must be a number │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ [1, 'a'] │
│ → N = 1 ✅ (1 is number) │
│ │
│ ['a', 1] │
│ → N = 'a' ❌ (not a number) │
│ → match fails → false branch │
│ │
└──────────────────────────────────────────────┘
Visual: Distribution with Infer
┌──────────────────────────────────────────────┐
│ type Unwrap<T> = T extends Promise<infer U> ? U : T│
│ │
│ Unwrap<Promise<string> | Promise<number>> │
│ │
└──────────────────────────────────────────────┘
│
│ distribute
▼
┌──────────────────────────────────────────────┐
│ Unwrap<Promise<string>> → string │
│ Unwrap<Promise<number>> → number │
│ │
│ combine: string | number │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Prevent distribution: │
│ │
│ type WholeUnwrap<T> = │
│ [T] extends [Promise<infer U>] ? U : never│
│ │
│ WholeUnwrap<Promise<string> | Promise<number>>│
│ → the union itself isn't a Promise → never │
│ │
└──────────────────────────────────────────────┘
Visual: Recursion with Infer
┌──────────────────────────────────────────────┐
│ type DeepUnwrap<T> = │
│ T extends Promise<infer U> ? DeepUnwrap<U> : T│
│ │
│ DeepUnwrap<Promise<Promise<string>>> │
│ │
└──────────────────────────────────────────────┘
│
│ step 1
▼
┌──────────────────────────────────────────────┐
│ T = Promise<Promise<string>> │
│ matches Promise<infer U> │
│ U = Promise<string> │
│ recurse: DeepUnwrap<Promise<string>> │
│ │
└──────────────────────────────────────────────┘
│
│ step 2
▼
┌──────────────────────────────────────────────┐
│ T = Promise<string> │
│ matches Promise<infer U> │
│ U = string │
│ recurse: DeepUnwrap<string> │
│ │
└──────────────────────────────────────────────┘
│
│ step 3
▼
┌──────────────────────────────────────────────┐
│ T = string │
│ doesn't match → return T │
│ │
│ result: string │
│ │
└──────────────────────────────────────────────┘
Visual: Reverse Tuple
┌──────────────────────────────────────────────┐
│ type Reverse<T> = │
│ T extends [infer F, ...infer R] │
│ ? [...Reverse<R>, F] │
│ : []; │
│ │
│ Reverse<[1, 2, 3]> │
│ │
└──────────────────────────────────────────────┘
│
│ F = 1, R = [2, 3]
▼
┌──────────────────────────────────────────────┐
│ [...Reverse<[2, 3]>, 1] │
│ │
│ Reverse<[2, 3]> │
│ F = 2, R = [3] │
│ [...Reverse<[3]>, 2] │
│ │
│ Reverse<[3]> │
│ F = 3, R = [] │
│ [...Reverse<[]>, 3] = [3] │
│ │
│ back up: [3, 2] then [3, 2, 1] │
│ │
└──────────────────────────────────────────────┘
Visual: When to Use Which
┌──────────────────────────────────────────────┐
│ Extract a specific piece of a type? │
│ │ │
│ └── Use infer with the right pattern │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Property type with known key? │
│ │ │
│ └── Use T[K] (indexed access) │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Recursive extraction? │
│ │ │
│ └── infer + recursion + base case │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Filter union by shape? │
│ │ │
│ └── infer + distribution │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Match whole union? │
│ │ │
│ └── Wrap in [T] extends [U] │
│ │
└──────────────────────────────────────────────┘
Visual: Common Infer Patterns
┌──────────────────────────────────────────────┐
│ Unwrap promise │
│ ─ T extends Promise<infer U> ? U : T │
│ │
│ Function return │
│ ─ T extends (...a) => infer R ? R : never │
│ │
│ Function params │
│ ─ T extends (...a: infer P) => any ? P : never│
│ │
│ Array element │
│ ─ T extends (infer U)[] ? U : never │
│ │
│ First tuple │
│ ─ T extends [infer F, ...] ? F : never │
│ │
│ Last tuple │
│ ─ T extends [..., infer L] ? L : never │
│ │
│ Property type │
│ ─ T extends { k: infer V } ? V : never │
│ │
└──────────────────────────────────────────────┘
Visual: Decision Flow
┌──────────────────────────────────────────────┐
│ Need to extract a type? │
│ │ │
│ └── Use infer in a conditional pattern │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Which position? │
│ │ │
│ ├── Promise value → Promise<infer U>│
│ ├── Array element → (infer U)[] │
│ ├── Function return → ... => infer R │
│ ├── Function params → ...args: infer P│
│ ├── Tuple first → [infer F, ...] │
│ ├── Tuple last → [..., infer L] │
│ └── Object property → {k: infer V} │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Need a constraint? │
│ │ │
│ └── infer X extends Y │
│ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ Recursion? │
│ │ │
│ └── Include a base case for the false branch│
│ │
└──────────────────────────────────────────────┘
Summary
| Concept | Meaning |
|---|---|
infer X | Capture a type in a pattern |
infer X extends Y | Capture with constraint |
Multiple infer | Capture several positions |
| Recursion | Self-reference with base case |
| Distribution | infer per union member |
[T] extends [U] | Prevent distribution |
...infer Rest | Capture the rest |
Key takeaways:
infercaptures part of a type when a pattern matches- It only appears in the true branch’s pattern of a conditional type
- Positions — array elements, promise values, function params and returns, tuple slots, object properties
infer X extends Yconstrains the captured type — useful for filtering during capture- Distribution applies
inferper union member —Unwrap<Promise<string> \| Promise<number>>givesstring \| number [T] extends [U]prevents distribution — match the whole union- Recursion works — provide a base case in the false branch
- Standard utilities —
ReturnType,Parameters,Awaited,InstanceType— areinferpatterns ReverseandDeepUnwrapshow recursiveinferin action- Multiple
infers capture multiple positions at once —[infer A, infer B] - Reach for
inferwhen you’re pattern matching and extracting; useT[K]for simple property access
Remember: infer turns a conditional type from a test into an extraction. It reads the shape of a type and binds parts of it to variables you can use. Every utility that pulls something out of another type uses it — the standard library’s ReturnType, Parameters, Awaited, InstanceType are all the same pattern. Learn the positions, constrain when needed, respect distribution, and include a base case in recursion. Once you can write infer patterns, you can build any type-level extraction you’ll ever need.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!