ESC

Type to search the knowledge base.

Conditional Types Intro

T extends U ? X : Y — how TypeScript picks types from conditions, distributes over unions, and powers utility types you already use.

advanced4 min read
  • 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

  1. any short-circuits — any extends X is true and false in many distributions; avoid feeding any into clever conditionals.
  2. Over-abstracted helpers — if only one call site uses it, write the concrete type.
  3. Recursive conditionals — depth limits exist; prefer Awaited / iterative designs.
  4. 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 + keyof before 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.”

Further reading

Related guides