Conditional Types Intro
T extends U ? X : Y — how TypeScript picks types from conditions, distributes over unions, and powers utility types you already use.
- typescript
- conditional-types
Conditional types let the type system branch: if a type parameter matches a pattern, produce one type; otherwise another. You already depend on them — NonNullable, ReturnType, Extract, and half of modern library typings are conditionals under the hood.
type IsString<T> = T extends string ? true : false;
type A = IsString<'hi'>; // true
type B = IsString<42>; // false
Docs: Conditional Types, infer.
Mental model
Read T extends U ? X : Y as: “Can I use a value of type T where U is expected? If yes, result is X; else Y.”
type ApiResult<T> = T extends { error: string }
? { ok: false; error: string }
: { ok: true; data: T };
Useful when transforming DTO shapes or building overloaded library APIs without writing many overloads by hand.
infer — pull a piece out
type ElementOf<T> = T extends (infer E)[] ? E : never;
type N = ElementOf<number[]>; // number
type S = ElementOf<string[]>; // string
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
type R = ReturnOf<() => Promise<User>>; // Promise<User>
Prefer built-ins when they exist: ReturnType<F>, Parameters<F>, Awaited<T>.
See infer keyword basics.
Distributive conditionals
Naked type parameters distribute over unions:
type ToArray<T> = T extends any ? T[] : never;
type X = ToArray<string | number>;
// string[] | number[] — not (string | number)[]
That is often what you want (Exclude, Extract work this way). To disable distribution, wrap in a tuple:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Y = ToArrayNonDist<string | number>; // (string | number)[]
Knowing distribution explains weird results when filtering unions.
Built-in utilities that are conditionals
type T0 = NonNullable<string | null | undefined>; // string
type T1 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // 'a'
type T2 = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
Simplified sketches:
type MyNonNullable<T> = T extends null | undefined ? never : T;
type MyExtract<T, U> = T extends U ? T : never;
type MyExclude<T, U> = T extends U ? never : T;
Frontend-shaped example: props with/without children
type PropsWithRequiredChildren<P> = P extends { children?: infer C }
? Omit<P, 'children'> & { children: C extends undefined ? React.ReactNode : C }
: P & { children: React.ReactNode };
More practical day-to-day: normalize async return types:
type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T;
type Data = Unwrap<Promise<Promise<User>>>; // User
(Awaited<T> is the stdlib version.)
Filtering keys with conditionals + mapped types
type FunctionKeys<T> = {
[K in keyof T]-?: T[K] extends (...args: never[]) => unknown ? K : never;
}[keyof T];
type Methods = FunctionKeys<{
id: string;
save(): void;
reload(): Promise<void>;
}>; // 'save' | 'reload'
This pattern powers typed form libraries and “pick only event handlers” helpers.
Footguns
anyshort-circuits —any extends Xis true and false in many distributions; avoid feedinganyinto clever conditionals.- Over-abstracted helpers — if only one call site uses it, write the concrete type.
- Recursive conditionals — depth limits exist; prefer
Awaited/ iterative designs. - Reading errors — conditional failures produce long messages; intermediate type aliases help debugging.
// Debug by aliasing steps
type Step1 = Foo<Bar>;
type Step2 = Baz<Step1>;
When not to reach for conditionals
- Simple unions and overloads already express the API.
- Runtime branching belongs in runtime code — conditionals don’t execute.
- Prefer mapped types +
keyofbefore inventing a mini type-language.
Interview out-loud answer
“Conditional types are ternary types: T extends U ? X : Y. They distribute over unions when T is bare, which is how Exclude/Extract work. infer peels nested pieces like array elements or return types. I use them for library-grade utilities, not for everyday component props.”
Related on this site
- infer keyword basics
- Mapped types intro
- Utility types Partial Pick Omit
- Generics basics
- never type and exhaustiveness
Further reading
Related guides
- Basic Types and AnnotationsPrimitives, arrays, objects, and function annotations TypeScript actually checks — plus where inference is enough and where it is not.
- Branded Types for IDsNominal-style UserId vs OrderId in TypeScript — prevent ID mixups at compile time with brands, parsers, and form boundaries.
- Declaration Files and DefinitelyTypedHow .d.ts files describe JS to TypeScript, when to use @types packages, module augmentation, and writing minimal ambient types for untyped libs.
- Discriminated Unions for UI StateModel loading, success, and error as mutually exclusive variants so TypeScript and your UI cannot show impossible states.
- Enums vs Union TypesWhen TypeScript enums help, when they hurt, and why string literal unions plus const objects win for most frontend code.