Lesson 17 of 25
Conditional Types
Types that branch on a condition with extends ? :, how they distribute automatically over unions, and extracting a nested type with infer.
Everything covered so far describes a fixed shape. A conditional type lets a
type itself branch, based on a check against another type — the type-level
equivalent of an if/else, evaluated entirely at compile time.
The basic form
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // falseextends here doesn't mean class inheritance — it means "is T assignable
to string?", the exact same meaning extends had as a generic constraint
earlier in this track. The conditional type reads left to right: if the
check holds, resolve to the first branch; otherwise, the second.
A more useful example
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null>; // string
type B = NonNullable<number>; // numberThis is, in fact, extremely close to how the built-in NonNullable<T> from
the utility types lessons is actually implemented — a conditional type that
resolves to never (removing that case) whenever T is null or
undefined.
Distributive conditional types
Something specific and worth understanding deliberately happens when T in
T extends U ? X : Y is a naked type parameter and you apply the
conditional to a union: TypeScript checks each member of the union
separately, then unions the results back together, rather than checking
the whole union as one combined type:
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>;
// Distributes to: ToArray<string> | ToArray<number>
// Which becomes: string[] | number[]This is why NonNullable<string | null> above correctly strips out just the
null case and keeps string, rather than the whole union collapsing to
never the moment any member fails the check — each member is judged on
its own.
infer: capturing part of a type
infer introduces a new type variable inside a conditional type's
condition, letting you pull a piece out of a matched structure without
already knowing what it is:
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<number>; // number — T didn't match Promise<...>, so T itselfRead it as: "if T is some Promise wrapping something, capture that
something as U and resolve to it; otherwise, just resolve to T
unchanged." This is genuinely how Awaited<T>, covered in the utility types
lessons, is implemented (with extra handling for nested promises and
thenables) — and it's the same mechanism behind ReturnType<T>:
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { name: "Amara" };
}
type User = MyReturnType<typeof getUser>; // { name: string }infer R here captures whatever the function's return type actually is,
using the general shape (...args: any[]) => infer R as the pattern to
match against.
Where this shows up in practice
You will very rarely need to write a conditional type with infer in
everyday application code — but recognising the pattern matters, because
it's exactly how a large portion of the utility types you already use
(ReturnType, Parameters, Awaited, NonNullable) are actually defined
under the hood. Understanding conditional types is what turns those from
"magic built-ins" into "ordinary type-level code you could have written
yourself."
Try it yourself
What to remember
- A conditional type (
T extends U ? X : Y) branches at compile time based on whether T is assignable to U. - When T is a bare type parameter and the check is applied to a union, the conditional distributes over each member separately before unioning the results.
infercaptures part of a matched type's structure into a new type variable, usable in the true branch.- Several familiar utility types (
ReturnType,Awaited,NonNullable) are themselves ordinary conditional types usinginfer.
Check yourself
4 questions · pass 3/4 to unlock Mapped Types
1.What does
type IsString<T> = T extends string ? true : false;evaluate to forIsString<"hi">?2.Given a conditional type
T extends U ? X : Y, what happens when T is a union likeA | B?3.What does
inferdo inside a conditional type, e.g.T extends Promise<infer U> ? U : never?4.What does
ReturnType<T>(from the utility types lessons) actually use internally to extract a function's return type?
4 left to answer