Lesson 20 · Module 3.3 — Type-level programming
T extends U ? X : Y, and inferYou 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.
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.
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.
infer exists: declare the variable you want to captureHere 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.
ReturnTypeThis 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.
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.
Recall, don’t re-read. (Answers reveal on click.)
infer R do inside a conditional type's extends clause?type R<T> = T extends (...a: any[]) => R ? R : any fail?T extends string ? "yes" : "no" with no infer, the conditional is doing…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.
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.