infer looks like a single feature: a placeholder you drop into the extends clause of a conditional type, and the compiler fills it in. But the same keyword behaves differently depending on how many you write, where you put them, and what variance the surrounding position carries. Inferring U from a union produces a different kind of thing than inferring U from a function parameter. That distinction does real work in libraries you already depend on.

The two you already know

The canonical use pulls a type out of a structure you match against. ReturnType is the whole idea in one line:

type ReturnType<T> = T extends (...args: any) => infer R ? R : never;

type A = ReturnType<() => string>; // string

The infer R introduces a fresh type variable. It binds to whatever occupies the return position when T matches the function shape, then becomes available in the true branch. Element extraction from an array is the same move in a different slot:

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

type B = ElementOf<number[]>; // number
type C = ElementOf<string[]>; // string

Note the pattern is the mutable (infer E)[]. A readonly string[] does not match it, so it falls to the false branch and resolves to never. To accept readonly inputs you would write T extends readonly (infer E)[] ? E : never.

The real behavior starts when you write more than one infer in a single pattern, or when the position you infer from has variance.

Several captures at once

A pattern can bind multiple variables, and tuple destructuring is where this earns its keep. Splitting a tuple into head and tail is structurally identical to const [head, ...tail] = arr:

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

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

type H = Head<[1, 2, 3]>; // 1
type R = Tail<[1, 2, 3]>; // [2, 3]

The rest element ...infer Rest captures everything the fixed positions did not. You are not limited to the front, either. A pattern can fix both ends and let the spread float in the middle, which is how you grab the last element without counting:

type Last<T extends readonly unknown[]> =
  T extends [...infer _, infer L] ? L : never;

type L = Last<[1, 2, 3]>; // 3

That [...infer _, infer L] is a useful shape. There is no index arithmetic. The compiler unifies the spread against the leading portion and L against the final slot. The same two-ended trick drives the bracket matching in Valid Parentheses, where the type peels characters off a string and pushes them onto a stack tuple one at a time.

Constraining the capture

Before TypeScript 4.7, narrowing an inferred type meant a second conditional. To get the first element of a tuple but only when it was a string, you inferred it, then tested it:

// Before 4.7: infer, then check in a nested conditional
type FirstString<T> =
  T extends [infer H, ...infer _]
    ? H extends string ? H : never
    : never;

TypeScript 4.7 folded the test into the capture. infer R extends string only matches when the inferred type satisfies the constraint, so the nested conditional disappears:

// 4.7+: constrained infer
type FirstString<T> =
  T extends [infer H extends string, ...infer _] ? H : never;

type D = FirstString<['hi', 1]>; // 'hi'
type E = FirstString<[1, 'hi']>; // never (head isn't a string)

The payoff is more than fewer lines. Constrained infer also narrows the captured type rather than leaving it widened, which matters when parsing string literals into numbers. Without the constraint, the template-literal capture is always string. With extends number the compiler coerces and narrows in the same step:

type ToNumber<S extends string> =
  S extends `${infer N extends number}` ? N : never;

type F = ToNumber<'42'>; // 42  (a number literal, not string)
type G = ToNumber<'x'>;  // never

Two Sum leans on exactly this to turn its string-encoded input into numeric literals it can add. Skip the constraint and you are stuck with string. Add it and the literal flows through as a real number.

Position decides union or intersection

The same variable name, inferred from two places, combines differently depending on the variance of those places.

Infer a variable from a covariant position more than once and the captures join as a union. Object property positions are covariant, so a T matched against either arm of a union contributes from both arms:

type ValueOf<T> = T extends { a: infer U } ? U : never;

type Cov = ValueOf<{ a: string } | { a: number }>; // string | number

The conditional distributes over the union, infers U from each member, and the results join. Covariant repetition produces a union.

Now move the same variable into a contravariant position: a function parameter. Function arguments are contravariant. When a single infer variable appears in multiple contravariant positions, the compiler combines the candidates as an intersection instead. That is the entire mechanism behind the well-known UnionToIntersection:

type UnionToIntersection<U> =
  (U extends any ? (arg: U) => void : never) extends
    (arg: infer I) => void ? I : never;

type Inter = UnionToIntersection<{ a: 1 } | { b: 2 }>;
// { a: 1 } & { b: 2 }

Walk it in two steps. The inner U extends any ? (arg: U) => void : never distributes over the union and yields ((arg: { a: 1 }) => void) | ((arg: { b: 2 }) => void). Then infer I sits in the parameter slot of that union of functions. To find a single type assignable from every member’s parameter, the compiler intersects them, because a function is assignable to another only when its parameter is a supertype of the other’s. So I resolves to { a: 1 } & { b: 2 }.

Here is the structural fact underneath. Covariant inference asks “what type is produced by all these positions?” and answers with a union. Contravariant inference asks “what type is accepted by all these positions?” and answers with an intersection. Same keyword, opposite combinator, chosen by where you wrote it.

What to take from it

infer is one keyword wearing several hats. It extracts a single type from a shape. It binds several at once when a pattern has multiple holes. It narrows in place once you give it a constraint. And it switches between union and intersection results based on the variance of the slot it lands in. These are not separate features. They fall out of how the compiler unifies a pattern against a type and how it reconciles multiple candidates for the same variable.

Most of them collapse into one underlying behavior once you notice that conditional types distribute over unions by default. That distribution is its own subject: see distributive conditional types for why T extends any ? ... : ... quietly maps over every member of a union.