Conditional types
A conditional type chooses one type or another based on a condition — an if/else for types. It is
the most powerful type-level tool in TypeScript, and it is what lets a type react to its input:
"if T is a string, do this, otherwise that". Combined with the infer keyword (next lesson),
conditional types can extract and transform types in ways that feel almost like computation — because
they are.
The syntax: T extends U ? X : Y
A conditional type reads like a ternary, using extends as the condition:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
type C = IsString<"Pune">; // true — "Pune" is a string
T extends string ? true : false means "if T is assignable to string, the type is true,
otherwise false." The extends here is a check — "is T a subtype of string?" — not the
inheritance extends. Read it as a question: does T fit string? If yes, take the first branch;
if no, the second. This is a type that computes a different type depending on its input — the
foundation of everything advanced.
A practical example: unwrapping
Conditional types shine at "if the input is wrapped in X, give me what is inside, otherwise give me the input":
type ElementType<T> = T extends (infer U)[] ? U : T;
type A = ElementType<string[]>; // string — it was an array, so give the element type
type B = ElementType<number[]>; // number
type C = ElementType<boolean>; // boolean — not an array, so give it back unchanged
ElementType<T> says "if T is an array of some element type, return that element type; otherwise
return T itself." (The infer U — extracting the element type — is the next lesson; for now, read it
as "the element type".) So ElementType<string[]> is string, but ElementType<boolean> is just
boolean. This is a type that adapts to its input — impossible without conditional types.
Filtering unions — distribution
Here is the single most surprising and useful behaviour of conditional types: when the checked type T
is a union, the conditional distributes over each member of the union, checking each one
separately and combining the results:
type NonNullable<T> = T extends null | undefined ? never : T;
type A = NonNullable<string | null | number | undefined>;
// distributes: check string (keep), null (-> never, dropped), number (keep), undefined (-> never, dropped)
// result: string | number
NonNullable<T> — the built-in utility type — is a conditional that maps null/undefined to never
(which vanishes from a union) and everything else to itself. When you apply it to a union, TypeScript
runs the conditional on each member independently and unites the results — so null and undefined
become never (and drop out), while string and number are kept. This distribution over unions
is how conditional types filter and transform unions, and it is the mechanism behind Exclude,
Extract, and much more:
type Exclude<T, U> = T extends U ? never : T; // keep members of T NOT in U
type Extract<T, U> = T extends U ? T : never; // keep members of T that ARE in U
type A = Exclude<"a" | "b" | "c", "b">; // "a" | "c"
type B = Extract<"a" | "b" | "c", "a" | "z">; // "a"
Exclude<"a" | "b" | "c", "b"> distributes the conditional over each of "a", "b", "c": "a" is
not "b" (keep), "b" is "b" (→ never, drop), "c" is not "b" (keep) — result "a" | "c". These
are the real definitions of two more built-in utility types, and now you can read them.
Nested conditionals — an if/else-if chain
Conditionals can nest, forming a chain that maps inputs to outputs — a type-level switch:
type TypeName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type A = TypeName<string>; // "string"
type B = TypeName<() => void>; // "function"
type C = TypeName<string[]>; // "object"
Each extends ? : is one branch of the chain; the final "object" is the else. This reads like a
type-level version of the runtime typeof operator, and it is a common shape for mapping a category of
type to a label or a different type.
infer preview, and where this is heading
The full power of conditional types comes with infer (the next lesson), which lets you capture a
type from inside the checked type:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
That infer R says "if T is a function, capture its return type as R and use it" — which is exactly
how the built-in ReturnType utility works. You will meet infer properly next; the point here is that
conditional types plus infer are how TypeScript extracts types from structures — a function's return
type, a promise's resolved type, an array's element type, a tuple's first element. It is genuine
computation over types.
When to reach for a conditional type
Be honest about this: you will read conditional types far more often than you write them. They power
utility types and library type definitions, and understanding them is essential for reading modern
TypeScript. But in application code, you write your own conditional type only occasionally — when you
genuinely need a type that adapts to its input in a way keyof, mapped types, and generics cannot
express. Reaching for a hand-rolled conditional type for something simpler is over-engineering (the same
lesson the design-patterns module teaches). So: learn to read them fluently, reach for the built-in
ones (NonNullable, Exclude, Extract, ReturnType) freely, and write your own only when a real
need appears. When you do, they are astonishingly powerful — but power used unnecessarily is just
complexity.
Check your work
What a conditional type is. An if/else for types — T extends U ? X : Y — that computes a
different type based on its input.
How to read extends in a conditional. As a check — "is T assignable to U?" — not inheritance.
What distribution over unions is. When the checked type is a union, the conditional runs on each member separately and combines the results — the mechanism behind filtering unions.
How NonNullable, Exclude, Extract work. Conditionals that map unwanted members to never
(which drops from a union) and keep the rest — you can now read their definitions.
Nested conditionals. A chain of extends ? : forming a type-level switch/if-else-if.
What infer adds (preview). Captures a type from inside the checked type — a function's return, a
promise's value — genuine extraction, covered next.
The honest guidance. Read conditional types fluently and use the built-in ones freely; write your own only when a real need appears — otherwise it is over-engineering.
Practice
- Write
IsString<T>and test it onstring,number, and a string literal. - Write
NonNullable<T>yourself and apply it tostring | null | undefined. Confirm the result isstring. - Write
Exclude<T, U>andExtract<T, U>yourself and test each on a literal union. - Apply a conditional to a union and observe distribution — trace how each member is checked.
- Write the nested
TypeName<T>chain and test several inputs. - Read the built-in
ReturnType's definition and identify the conditional and theinfer. - Find a place you were tempted to write a conditional type and decide whether a built-in utility or a simpler tool would do instead.
Official documentation
- TypeScript — Conditional Types — The full feature.
- TypeScript — Distributive Conditional Types — Distribution over unions.
- TypeScript — Utility Types (Exclude, Extract, NonNullable) — Conditional types you can now read.
Next: infer — extracting types from within other types.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship