| |

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 T against the pattern “an array of something”
  • Bind the “something” to U
  • In the true branch, U is 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 infer is scoped to the true branch: The pattern on the left of extends is 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 infer was added in TypeScript 2.8. Constraints came in 4.7. Until then, extracting a constrained type required a two-step conditional: capture with infer, 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 infer with recursion is powerful: DeepUnwrap and Reverse show that infer composes 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 infer are intertwined: Both are about unions. Distribution splits a union into members; infer captures per member. Combined, they give you Map-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:

  • Unwrap and DeepUnwrap — promise value, recursively
  • MyReturnType, MyParameters, MyFirstParam — pieces of a function type
  • AsyncReturnType — unwrapping an async function’s result
  • ElementOf, FirstOf, LastOf — array and tuple positions
  • Reverse — recursive tuple manipulation
  • ValueOf, KeysOfType — object value types
  • FirstNumber — constrained infer

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 infer and 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

FormMeaning
infer XCapture a type
infer X extends YCapture with constraint
infer X, infer YMultiple captures
...infer RestCapture the rest

Positions

PatternCaptures
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) => anyParameter tuple
T extends (...args: any) => infer RReturn 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 IInstance type

Standard Library

Typeinfer 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

PatternPurpose
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

FormEffect
infer N extends numberOnly matches numbers
infer S extends stringOnly matches strings
infer F extends FunctionOnly matches functions
infer A extends any[]Only matches arrays

Distribution Rules

Left sideDistribution
T (naked)Distributes
[T]Doesn’t distribute
Concrete typeDoesn’t distribute

Distribution with infer

InputPatternResult
Promise<string> | Promise<number>Promise<infer U>string | number
Promise<string> | numberPromise<infer U>string | number
[Promise<string> | Promise<number>][Promise<infer U>]never

Recursion

TypeBase case
DeepUnwrap<T>Not a Promise → return T
Reverse<T>Empty tuple → return []
ExtractNumbers<T>Empty tuple → return []

infer vs Indexed Access

ApproachUse for
T[K]Property type when K is known
inferExtracting when pattern matching

Error Cases

ErrorCauseFix
infer not allowedWrong positionMove to true branch
Cannot find name XUsed outside scopeMove into branch
Infinite recursionNo base caseAdd terminal
Wrong capturePattern mismatchAdjust pattern

Utility Combinations

TypeResult
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

SituationUse 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

PitfallProblemSolution
infer outside conditionalSyntax errorMove inside
infer in false branchSyntax errorUse true branch only
Distribution surpriseWrong result for unionsWrap in [ ]
Missing base caseInfinite recursionAdd terminal case
Over-constrained inferNever matchesLoosen constraint
Forgot readonlyDoesn’t match readonly arraysUse readonly (infer U)[]
Wrong pattern shapeNever matchesMatch the structure
Confused with T[K]Wrong toolUse indexed access
Recursion too deepType complexitySimplify

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

ConceptMeaning
infer XCapture a type in a pattern
infer X extends YCapture with constraint
Multiple inferCapture several positions
RecursionSelf-reference with base case
Distributioninfer per union member
[T] extends [U]Prevent distribution
...infer RestCapture the rest

Key takeaways:

  • infer captures 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 Y constrains the captured type — useful for filtering during capture
  • Distribution applies infer per union member — Unwrap<Promise<string> \| Promise<number>> gives string \| number
  • [T] extends [U] prevents distribution — match the whole union
  • Recursion works — provide a base case in the false branch
  • Standard utilitiesReturnType, Parameters, Awaited, InstanceType — are infer patterns
  • Reverse and DeepUnwrap show recursive infer in action
  • Multiple infers capture multiple positions at once — [infer A, infer B]
  • Reach for infer when you’re pattern matching and extracting; use T[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!