RizTech Academy logo
RizTech Academy
The Type System in DepthLesson 3 of 635 min

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

  1. Write IsString<T> and test it on string, number, and a string literal.
  2. Write NonNullable<T> yourself and apply it to string | null | undefined. Confirm the result is string.
  3. Write Exclude<T, U> and Extract<T, U> yourself and test each on a literal union.
  4. Apply a conditional to a union and observe distribution — trace how each member is checked.
  5. Write the nested TypeName<T> chain and test several inputs.
  6. Read the built-in ReturnType's definition and identify the conditional and the infer.
  7. 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

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