Conditional Types

Advanced · 20 min read · ▶ live playground · ✦ checkpoint

Conditional types are TypeScript's type-level if. T extends U ? X : Y asks, "does type T fit inside type U?" If yes, the result is X; if no, the result is Y.

Use them when a type should choose a result from another type instead of copying a field directly.

The type-level if

A conditional type is a type expression with three parts: a check, a true branch, and a false branch.

type SupportRoute<T> = T extends { plan: "pro" } ? "priority" : "standard";

Read it left to right: "for some type T, if T has a pro plan shape, produce "priority"; otherwise produce "standard"."

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

No value-level if ran here. SupportRoute<FreeUser> became "standard" before runtime, and SupportRoute<ProUser> became "priority". The emitted JavaScript only sees the strings you kept as values.

The word extends still means assignability. It is not class inheritance here. It asks whether one type fits inside another type's allowed set.

Unions distribute

Conditional types have one rule that makes them powerful and surprising. When the checked type is a bare type parameter, meaning T by itself on the left of extends, a union is handled one member at a time.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

KeepStrings<"launch" | "billing" | 404 | false> is not checked as one big union. It is checked as four tiny questions:

  • "launch" extends string ? "launch" : never gives "launch".
  • "billing" extends string ? "billing" : never gives "billing".
  • 404 extends string ? 404 : never gives never.
  • false extends string ? false : never gives never.

never is the empty set, so it disappears from the final union. The result is "launch" | "billing".

The union does not always stay whole

Here is the classic surprise. You write IsString<string | number> and expect the answer to be "no" because the whole union is not assignable to string.

The next snippet uses type-test helpers, tiny aliases that intentionally fail when a type result is not what you expected.

type Equal<Left, Right> = (
  <Value>() => Value extends Left ? 1 : 2
) extends (
  <Value>() => Value extends Right ? 1 : 2
)
  ? (
      <Value>() => Value extends Right ? 1 : 2
    ) extends (
      <Value>() => Value extends Left ? 1 : 2
    )
    ? true
    : false
  : false;
 
type Expect<Condition extends true> = Condition;
 
type IsString<T> = T extends string ? "yes" : "no";
type Answer = IsString<string | number>;
 
type Check = Expect<Equal<Answer, "no">>;

→ playground.ts:20:21 - error TS2344: Type 'false' does not satisfy the constraint 'true'.

The bracket trick turns distribution off:

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

[T] is not about runtime arrays. It is a type-level wrapper that stops the conditional type from seeing a bare T. Now the checker asks one question: "is string | number assignable to string?" The answer is no.

Rebuild three tiny utilities

Section 12 turns TypeScript's built-in utility types into a daily toolkit. Here, rebuild three tiny versions only to see the mechanics.

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

All three are the same shape:

  • MyNonNullable<T> removes null and undefined by returning never for those members.
  • MyExtract<T, Match> keeps members assignable to Match.
  • MyExclude<T, Match> removes members assignable to Match.

Do not memorize these as new tools yet. The official versions come back in Section 12. The point today is the machine underneath: check each member, return the member you want to keep, or return never to remove it.

Conditional types choose branches. Next lesson adds infer, which lets a conditional type name a piece it found while checking a shape.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion