Lesson 20 · Module 3.3 — Type-level programming

Conditional types: T extends U ? X : Y, and infer

You met K extends "password" ? never : K last lesson without naming it. That is a conditional type — a type-level if. The new keyword here is infer: it declares a fresh type variable inside the extends check and captures whatever matches there. Together they let a type pattern-match and pull a piece out.

The one rule

T extends U ? X : Y reads “if T is assignable to U, the type is X, otherwise Y.” Put infer R somewhere in U to say “match a structure here and name the matched part R,” which you can then use in the X branch.

The conditional alone: an assignability test

Without infer, a conditional just tests assignability and picks a branch. Both branches are types you already wrote:

type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<"hello">;  // "yes"  — "hello" is assignable to string
type B = IsString<number>;   // "no"   — number is not

Notice the X and Y branches are fixed strings here. The conditional decides which branch, but it cannot reach inside T and grab a sub-type. For that you need infer.

Why infer exists: declare the variable you want to capture

Here is the right question to ask: why can't ReturnType just be written with a bare R? Try it — the compiler rejects it because R was never declared:

// ❌ error: R is undeclared — the compiler has no idea what R refers to
type ReturnType1<T> = T extends (...a: any[]) => R ? R : any;

// ✅ infer DECLARES R right where it should be captured
type ReturnType2<T> = T extends (...a: any[]) => infer R ? R : any;

Think of the compiler as a detective. Without infer you are saying “check whether T matches a function returning this R I already defined” — but you never defined one. With infer R you are saying “match a function, and figure out its return type — call that discovery R.” The keyword both introduces the variable and fills it from the match.

The canonical example: ReturnType

This is the standard-library definition, and it is worth reading slowly:

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

function getUser() { return { name: "Alice", age: 30 }; }

type U = ReturnType<typeof getUser>;
// resolves to:  { name: string; age: number }

Walk it: T is the type of getUser, a function. The pattern (...args: any[]) => infer R matches any function and binds R to its return type — here { name: string; age: number }. The match succeeds, so the type is the R branch.

Read the contrast, not just the success

Compare T extends infer R ? R : never (captures and returns whatever T is) with T extends SomeKnownType ? T : never (a plain assignability test against a type you named). The first discovers; the second only checks. infer is always the one introducing a new name inside extends.

Check yourself

Recall, don’t re-read. (Answers reveal on click.)

What does infer R do inside a conditional type's extends clause?
Why does type R<T> = T extends (...a: any[]) => R ? R : any fail?
In T extends string ? "yes" : "no" with no infer, the conditional is doing…

Primary source

The official handbook page is TypeScript Handbook — Conditional Types, and the infer section is Inferring Within Conditional Types. Best way to feel it: paste the broken and fixed ReturnType into the TS Playground and read the error on the bare R.

Try it yourself

Got a conditional type and unsure what infer is grabbing? Paste it into the Playground and ask “what does R bind to here?” Next lesson (3.4) shows a surprising twist: when T is a union, a conditional secretly runs once per member — distributive conditional types.