1. The Syntax: T extends U ? X : Y#
A Conditional Type looks exactly like a JavaScript ternary operator (condition ? true : false). However, instead of evaluating a boolean value at runtime, it evaluates assignability using the extends keyword at compile time.
// "If T is assignable to string, evaluate to true. Else, evaluate to false."
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
The extends keyword in conditional types means assignability, not strict equality! If T is a specific string literal (like "hello"), it extends the broader string type, so the condition evaluates to true.
2. Dynamic Return Types (Replacing Overloads)#
The most practical everyday use case for conditional types is calculating a function’s return type dynamically based on the type of its input argument.
Imagine a database fetch function that takes either a single ID (string) or an array of IDs (string[]).
- If it receives a
string, it returns a singleUser. - If it receives a
string[], it returnsUser[].
Before conditional types, we had to use verbose Function Overloads (from the Fundamental module). With conditional types, we can achieve this in one elegant generic signature!
interface User { id: string; name: string }
// 1. The Conditional Type Logic
type FetchResult<T> = T extends string ? User : User[];
// 2. The Generic Function Signature
// We constrain T so the user can only pass a string or a string array.
function fetchUser<T extends string | string[]>(idOrIds: T): FetchResult<T> {
// Runtime implementation details...
return {} as any;
}
// 🟢 The compiler dynamically computes the exact return type!
const single = fetchUser("usr_1");
// Inferred Type: User
const multiple = fetchUser(["usr_1", "usr_2"]);
// Inferred Type: User[]
By linking the generic input T directly to the return type via a conditional evaluation, we eliminate overload boilerplate and create a perfectly typesafe API contract.
3. Nested Conditionals (Else-If Chains)#
Just like in JavaScript, you can nest ternary operators to create complex else-if chains in the type system. This allows you to construct massive type-level routers.
// A Type-Level router that extracts the primitive name of a type
type GetPrimitiveName<T> =
T extends string ? "string" :
T extends number ? "number" :
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object"; // The fallback
type T1 = GetPrimitiveName<"hello">; // "string"
type T2 = GetPrimitiveName<99.9>; // "number"
type T3 = GetPrimitiveName<() => void>; // "function"
type T4 = GetPrimitiveName<{ id: 1 }>; // "object"
4. Conditional Types in Real-World Utilities#
If you recall the Intermediate Module, this exact T extends U ? X : Y syntax is the engine that powers TypeScript’s built-in Extract and Exclude utility types!
// Exclude: If T extends U, return never (delete it). Else, return T.
type CustomExclude<T, U> = T extends U ? never : T;Summary & Next Steps#
In this episode:
- We introduced the ternary syntax of the type system:
T extends U ? X : Y. - We learned that conditions evaluate based on assignability, not strict equality.
- We used conditional types to create dynamic function return signatures, eliminating the need for overloads.
- We built
else-iflogic chains using nested conditionals.
This episode focused on feeding single types into conditionals. But what happens when you feed a Union Type ("A" | "B") into a generic T that is evaluated conditionally?
In Episode 42: Distributive Conditional Types, we will uncover the most confusing (and powerful) behavior in the TypeScript compiler!

