Reading — step 1 of 7
Learn
Conditional types let the type system make decisions. They look like ternaries but operate at the type level — and unlock most of TypeScript's type-system magic. The TypeScript Handbook treats them as the heart of advanced type-level programming.
Basic syntax
type IsString<T> = T extends string ? true : false;
type A = IsString<'hi'>; // true
type B = IsString<42>; // false
type C = IsString<string>; // true
T extends U ? X : Y reads: "if T is assignable to U, the type is X; otherwise Y."
Useful for branching:
type Stringable<T> = T extends number | boolean | string ? string : never;
Distribution over unions
The magic property: when T in T extends X ? ... is a naked union, the conditional distributes over each member:
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>;
// = (string extends any ? string[] : never) | (number extends any ? number[] : never)
// = string[] | number[]
Distribution lets you transform every member of a union independently. Disable distribution by wrapping in [T]:
type ToArray2<T> = [T] extends [any] ? T[] : never;
type B = ToArray2<string | number>; // (string | number)[]
Built-in conditional utilities
TypeScript ships several:
Exclude<T, U>— removes types from T assignable to U:type WithoutString = Exclude<string | number | boolean, string>; // number | booleanExtract<T, U>— keeps only types from T assignable to U:type OnlyString = Extract<string | number | boolean, string>; // stringNonNullable<T>— removes null and undefined:type Defined = NonNullable<string | null | undefined>; // string
infer — capture types within a conditional
The infer keyword introduces a type variable inside an extends clause:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type A = ReturnType<() => string>; // string
type B = ReturnType<(x: number) => boolean>; // boolean
type C = ReturnType<typeof Math.max>; // number
The infer R says "if T is a function type, capture its return type as R."
Common infer patterns:
// Get the parameters of a function:
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
// Get the element type of an array:
type ElementOf<T> = T extends (infer E)[] ? E : never;
type N = ElementOf<number[]>; // number
// Unwrap a Promise:
type Awaited<T> = T extends Promise<infer U> ? U : T;
type X = Awaited<Promise<string>>; // string
// First and rest of a tuple:
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type Tail<T extends any[]> = T extends [any, ...infer R] ? R : [];
These are the building blocks of every TypeScript utility library.
Real-world example — typed config
type HttpHandler<T> = T extends `GET ${string}`
? { get: () => Response }
: T extends `POST ${string}`
? { post: (body: any) => Response }
: never;
type GetHandler = HttpHandler<'GET /users'>; // { get: ... }
type PostHandler = HttpHandler<'POST /users'>; // { post: ... }
type Bad = HttpHandler<'PATCH /users'>; // never — caught at compile time
Route strings drive the shape of the handler — invalid routes become never, surfacing the bug.
Common mistakes
- Forgetting distribution —
Exclude<string | number, string>works because conditionals distribute. Wrapping in[T]disables it (sometimes desired). - Trying to
inferoutside an extends clause —inferonly appears inside the type afterextendsin a conditional. - Deep recursion in conditional types — TypeScript caps recursion. Use tail-call-style patterns (TS 4.7+) or iteration via tuple length.
- Mixing union and never —
T extends X ? Y : neveris the common pattern to FILTER unions; produces never for non-matches, which then disappear from the result. - Conditional types in regular code paths — they're powerful but make signatures hard to read. Use sparingly in public APIs.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…