open atlas
↑ Back to track
TypeScript type system, deep TS · 04 · 02

The infer keyword: pattern-match a type and capture a piece of it

`infer` declares a fresh type variable in a conditional's extends clause, bound to whatever fills that slot — the engine behind ReturnType, Awaited, and tuple head/tail. One infer name across several positions unions covariant candidates but intersects contravariant ones.

TS Middle ◷ 16 min
Level
FoundationsJuniorMiddleSenior

You use ReturnType<typeof fetchUser> to get the return type of a function you didn’t write, without ever naming it. Somewhere in lib.es5.d.ts is one line: type ReturnType<T extends (...a: any) => any> = T extends (...a: any) => infer R ? R : any. That infer R is the entire trick: it’s a hole in a pattern that says “match a function type, and whatever’s in the return slot — capture it as R.” TypeScript’s only built-in form of pattern matching.

By the end of this lesson you’ll be able to write ReturnType, Awaited, and tuple head/tail from memory — and you’ll know why the same infer name in two positions sometimes gives you a union and sometimes gives you never.

A hole in the pattern

When you need to extract a type that lives inside another type you don’t control — the return of a function, the resolved value of a Promise, the first element of a tuple — you could wrap and unwrap manually. Or you can punch a hole in the pattern and let TypeScript solve for it.

infer is only legal inside the extends clause of a conditional type. It declares a new type variable on the spot and lets TypeScript solve for it by matching structure:

type ElementOf<T> = T extends (infer E)[] ? E : never;

type A = ElementOf<string[]>;       // ^? string
type B = ElementOf<[1, 2, 3]>;      // ^? 1 | 2 | 3   (tuple element type)
type C = ElementOf<number>;         // ^? never       (not an array — false branch)

You wrote the shape (infer E)[] — “an array of something” — and TypeScript unifies it against the input, binding E to that something. E exists only in the true branch. This is the same machinery the standard library uses:

type Unwrap<T> = T extends Promise<infer V> ? V : T;
type R1 = Unwrap<Promise<number>>;  // ^? number
type R2 = Unwrap<string>;           // ^? string  (false branch returns T)

type Params<T> = T extends (...args: infer P) => unknown ? P : never;
type R3 = Params<(a: string, b: number) => void>; // ^? [a: string, b: number]

Tuple head, tail, and multiple infers in one pattern

You can place several infer variables in one pattern and capture distinct pieces simultaneously:

type Head<T extends readonly unknown[]> =
  T extends [infer H, ...unknown[]] ? H : never;
type Tail<T extends readonly unknown[]> =
  T extends [unknown, ...infer Rest] ? Rest : never;

type H = Head<[1, 2, 3]>; // ^? 1
type Te = Tail<[1, 2, 3]>; // ^? [2, 3]

type Last<T extends readonly unknown[]> =
  T extends [...unknown[], infer L] ? L : never;
type L = Last<[1, 2, 3]>; // ^? 3

Variadic tuple positions (...infer Rest) make head/tail/last trivial — and they’re the building block for the recursive type-level lists you’ll write in unit 05.

Constrained infer (TS 4.7): infer R extends ...

Before TS 4.7 you had to capture, then re-test in a nested conditional. Since 4.7 you can constrain the inference inline, which both validates and narrows the captured type:

// Capture a string-literal element and narrow it to string in one step
type FirstString<T> =
  T extends [infer S extends string, ...unknown[]] ? S : never;

type S1 = FirstString<["hello", 1, 2]>; // ^? "hello"
type S2 = FirstString<[1, "x"]>;        // ^? never   (first is not a string)

A practical payoff: infer N extends number lets you carry a numeric literal through without it widening, useful in numeric type arithmetic.

The variance twist: same name, multiple positions

This is the part people get wrong. When one infer name appears in multiple positions, TypeScript collects every candidate and combines them — but how depends on variance. In covariant positions (object property values, array elements, return types) the candidates are unioned:

type Co<T> = T extends { a: infer U; b: infer U } ? U : never;
type X = Co<{ a: string; b: number }>;
// ^? string | number   — covariant positions => UNION

In contravariant positions — function parameters — the candidates are intersected:

type Contra<T> =
  T extends { f: (x: infer U) => void; g: (x: infer U) => void } ? U : never;
type Y = Contra<{ f: (x: string) => void; g: (x: number) => void }>;
// ^? string & number   — contravariant positions => INTERSECTION  (here: never)
Why this works

The rule falls out of soundness. A captured U must be a type that works for every position it appeared in. In a covariant (output) position, a value of string | number is acceptable wherever either was produced — the union is the most precise type that covers both outputs. In a contravariant (input) position, a function that must accept string and a function that must accept number can only be safely fed a value that is acceptable to both — string & number. So the checker unions outputs and intersects inputs; it’s the same variance logic from unit 03, applied to inference candidates.

The cost side: infer is cheap to write, expensive to chain

A single infer is nearly free. The trap is recursive infer — peeling a tuple one head at a time, or unwrapping nested wrappers — because each recursion is a fresh conditional instantiation. TypeScript caps non-tail recursive instantiation at an instantiation depth of about 50 before it gives up with Type instantiation is excessively deep and possibly infinite; tail-recursive forms get further, to roughly 1000 levels. A tuple infer-walk over a 2000-element array literal will hit that wall, and the symptom isn’t a crash — it’s an any silently substituted at the bottom of the recursion, which then poisons every downstream type that depended on it.

The senior breakpoint: infer is the right tool when the piece you need is genuinely inside a type someone else owns — a library’s ReturnType, an Awaited over an unknown promise. It is the wrong tool when you control the type and could just name the piece. type UserId = User["id"] is an indexed access — instant to resolve, trivially hoverable — versus type UserId = Extract<...infer...> which the reader must mentally execute. And when you find yourself writing a 30-line recursive infer to parse a string or a deeply nested shape, that is the signal that the type system is being asked to do a parser’s job: a small codegen/build step that emits a flat named type compiles in microseconds and gives teammates an artifact they can read, where the recursive infer costs seconds of check time and produces an error message no one can decode.

lesson.inset.warning

Measure before you trust a clever infer. Run tsc --extendedDiagnostics and watch Instantiation count and Check time: a recursive infer that looks innocent can push Instantiation count into the millions and add 5–15s to a cold check on a real project, and because the same checker powers the editor, IntelliSense on the affected file degrades from instant to multi-second per keystroke. One under-constrained recursive infer over a large union can also tip you into the 100,000-member union cap, surfacing as Expression produces a union type that is too complex to represent — at which point the captured type degrades to an error/any rather than the precise type you intended.

Quiz

For type Contra<T> = T extends { f: (x: infer U) => void; g: (x: infer U) => void } ? U : never, what is Contra<{ f: (x: string) => void; g: (x: number) => void }>?

Order the steps

Order how TypeScript resolves ReturnType<F> for F = (a: number) => string, given ReturnType<T> = T extends (...a: any) => infer R ? R : any:

  1. 1 Test whether F is assignable to the function pattern (...a: any) => (something)
  2. 2 Unify F against the pattern, treating infer R as the hole in the return slot
  3. 3 Bind R to the matched return type, string
  4. 4 Take the true branch and return R, so ReturnType<F> is string
Recall before you leave
  1. 01
    Where is `infer` allowed, what does it do, and how would you write ElementOf and ReturnType with it?
  2. 02
    When one infer name appears in multiple positions, when do you get a union and when an intersection, and why?
  3. 03
    What did TS 4.7 add with constrained infer, and why is it more than syntax sugar?
Recap

infer turned the conditional from a yes/no test into a capture tool — you can now pull the element out of an array, the value out of a Promise, the head and tail off a tuple, and you know that repeating an infer name unions outputs but intersects inputs. Next, mapped types stop pattern-matching a single shape and start iterating one: { [K in keyof T]: ... } walks every key of a type to transform its values, with modifier surgery (?, readonly) and the homomorphic property that quietly preserves arrays and tuples. Now when you see ReturnType, Awaited, or any type that reaches inside a library type you don’t own, you’ll recognize the infer hole — and know exactly which position determines whether you get a union or an intersection.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 8 done
Connected lessons

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.