skip to content

How do you build a type-level switch out of nested TypeScript conditional types, and why does the order of the branches change the result?

level: middleimportance: should knowfreq 44%

answer

  1. chain them in the false branch
  2. first passing test wins
  3. specific before general
  4. a broad branch swallows the narrow one
  5. never marks the no-match case

basics

~20 s

Chain conditionals in the false branch so the first test that passes wins. Put narrow tests before broad ones: because extends is an assignability test, a broad branch placed first swallows inputs that a later, narrower branch was meant to match.

solid answer

~40 s

You nest each new conditional in the previous one's false branch — `T extends A ? X : T extends B ? Y : never` — which reads top-to-bottom like a `switch`. Evaluation is strictly first-match-wins: the compiler tries each `extends` in order and stops at the first that passes, so ordering is semantic, not cosmetic. Because `extends` means "assignable to", a broad test also matches everything a narrower test would match; put `T extends { kind: string }` above `T extends { kind: 'circle'; radius: number }` and the circle case becomes unreachable. Order from most specific to most general, and end the chain with `never` rather than a permissive fallback so an unmatched input produces a type nothing can satisfy instead of a plausible-looking wrong answer.

code

typescript · 16 lines
typescript
type Label<T> =
  T extends { kind: 'circle'; radius: number } ? 'circle' :
  T extends { kind: string }                  ? 'tagged' :
  never;

type A = Label<{ kind: 'circle'; radius: number }>; // 'circle'
type B = Label<{ kind: 'square'; side: number }>;   // 'tagged'
type C = Label<{ id: number }>;                     // never

// Reordered, the broad test swallows the specific one.
type Broken<T> =
  T extends { kind: string }                  ? 'tagged' :
  T extends { kind: 'circle'; radius: number } ? 'circle' :
  never;

type D = Broken<{ kind: 'circle'; radius: number }>; // 'tagged'

go deeper

for a junior

Know that conditionals chain by nesting in the false branch and that the first passing test wins. Be able to trace a short chain by hand for a concrete type argument.

for a middle

Explain why ordering is semantic: extends is an assignability test, so a broad branch matches everything a later narrow branch would, and specific tests must come first. Show the reordered, broken version.

for a senior

Demonstrate defensive habits — a never terminator, test types that assert the fallthrough case, and keeping chains short enough that the compiler's error messages stay readable.

for a principal

Be ready to set the standard: when a decision belongs in a chain of conditionals at all, when it should be a lookup type or a discriminated model instead, and what your team pays for it in diagnostics and onboarding.

## Chaining conditionals A conditional type has only two branches, so a multi-way decision is built by nesting another conditional into the false branch. Formatted one test per line it reads exactly like a `switch`: ```ts type Label<T> = T extends { kind: 'circle'; radius: number } ? 'circle' : T extends { kind: string } ? 'tagged' : never; ``` The nesting is right-associative, so no parentheses are needed; each `:` hands control to the next test. Nesting in the *true* branch is legal too — `T extends A ? (T extends B ? X : Y) : Z` — but it expresses "and also" rather than "otherwise", and it is much harder to read. The false-branch chain is the idiom to reach for. ## First match wins The compiler evaluates the tests in written order and stops at the first one that passes. There is no notion of "best match", no specificity scoring, and no evaluation of later branches once one succeeds. That single rule is the whole semantics — and it is why the order is part of the meaning of the type, not a style choice. ```ts type A = Label<{ kind: 'circle'; radius: number }>; // 'circle' type B = Label<{ kind: 'square'; side: number }>; // 'tagged' type C = Label<{ id: number }>; // never ``` ## Why order breaks things Remember what `extends` means in this position: *is the left type assignable to the right one?* A broad test is satisfied by every input a narrow test would accept, because the narrow shape has everything the broad one requires and more. So a broad branch placed first shadows the narrow branches beneath it: ```ts type Broken<T> = T extends { kind: string } ? 'tagged' : T extends { kind: 'circle'; radius: number } ? 'circle' : never; type D = Broken<{ kind: 'circle'; radius: number }>; // 'tagged' — not 'circle' ``` `{ kind: 'circle'; radius: number }` is assignable to `{ kind: string }` — `'circle'` is a string, and the extra `radius` is not asked about — so the first branch fires and the circle case is dead code. TypeScript will not warn you: an unreachable conditional branch is a perfectly valid type. Nothing is red; the type is simply wrong. The rule that follows is short: **order from most specific to most general.** When two tests overlap, the one that accepts fewer inputs must come first. ## The last branch The final `:` needs something, and the choice communicates intent. - `never` means "no case matched". It is the type with no values, so nothing can ever be produced for an unmatched input, and the mistake surfaces at the point where the result is used — a value fails to be assignable, or an editor shows `never` where a real type was expected. - A permissive fallback such as `'unknown'`, `string` or `object` means "everything else is this". That is legitimate when the catch-all really is a case in your model, but it hides typos and genuinely unhandled shapes behind an answer that looks reasonable. A good default is `never` while you are designing the type, switching to a real fallback only once you can justify it. It converts a silent wrong answer into a visible dead end. ```ts type Answer = Label<{ id: number }>; // declare const a: Answer; // nothing can be assigned to never ``` ## Practical shape of a chain A few habits keep these readable: - One test per line, aligned `?` and `:`, most specific first. The visual column makes an out-of-order branch obvious during review. - Keep each test as tight as it needs to be; `T extends { kind: string }` is a very wide net and belongs near the bottom. - Give intermediate results names when a chain grows. A chain of eight branches inside one alias produces error messages that print the whole unresolved conditional, and readers stop reading. - Add a check for an input you expect to fall through, and assert it is `never`, so the dead-branch mistake is caught by the type checker rather than by a reader. ## What an interviewer is listening for The strong answer is short: nest in the false branch, first match wins, order specific-to-general because `extends` is assignability and a broad test subsumes a narrow one, and terminate with `never` so an unmatched input cannot quietly produce a usable type. Being able to produce the reordered, broken version on demand — and explain that the compiler is perfectly happy with it — is what separates recall from understanding.

  • Will TypeScript warn you that a later branch in the chain has become unreachable?
    No. An unreachable conditional branch is a valid type, so nothing is flagged — the chain simply returns the wrong answer. The practical defence is a test type: instantiate the alias with an input that should hit the shadowed branch and assert the result, so a reordering breaks the build.
  • Can you nest a conditional in the true branch instead, and when would you?
    Yes — `T extends A ? (T extends B ? X : Y) : Z` is valid. It expresses "A and also B", so it fits when a second question only makes sense once the first has passed. For a flat multi-way decision the false-branch chain is clearer, because it reads top-to-bottom like a switch.
  • Why prefer `never` over a permissive fallback as the final branch?
    `never` has no values, so an unmatched input produces a type nothing can satisfy and the mistake surfaces where the result is used. A fallback like `string` or `object` answers plausibly for inputs you never considered, which hides typos and unhandled shapes behind a type that looks fine.

saying these in an interview costs you the question

  • Puts the broadest test first and wonders why
  • Thinks the compiler picks the best-matching branch
  • Uses a permissive fallback that hides unmatched inputs
  • Expects a warning for an unreachable branch
  • Believes only one level of nesting is allowed

context