skip to content

How does Angular's @switch block match its @case values, and how do several order statuses share one @case body?

level: middleimportance: should knowfreq 50%

answer

  1. strict equality
  2. no fallthrough, no break
  3. consecutive case blocks
  4. only case and default inside

basics

~20 s

@switch evaluates its expression once and compares it with each @case value using ===, rendering the first match or @default. There is no fallthrough; stack consecutive @case blocks to share one body (supported since v21.1).

solid answer

~40 s

`@switch (status)` evaluates the expression once per check and compares it with each `@case (value)` using strict equality, `===`, in order. The first match renders; if nothing matches, `@default` renders, and without a `@default` nothing is shown. There is no fallthrough and no `break`: each `@case` body is self-contained. To give several values the same body, write consecutive `@case` blocks with the body on the last one, for example `@case ('paid') @case ('packed') { ... }`, supported since v21.1. The block may contain only `@case` and `@default` blocks (plus whitespace and comments); stray markup is a parse error, and there may be only one `@default`. Because matching is strict, a numeric status `1` never matches `@case ('1')`.

code

ts · 19 lines
ts
import { Component, input } from '@angular/core';

type OrderStatus = 'pending' | 'paid' | 'packed' | 'shipped' | 'cancelled';

@Component({
  selector: 'app-order-status-banner',
  template: `
    @switch (status()) {
      @case ('pending') { <p class="banner">Waiting for payment.</p> }
      @case ('paid')
      @case ('packed') { <p class="banner">We are preparing your order.</p> }
      @case ('shipped') { <p class="banner">On its way.</p> }
      @case ('cancelled') { <p class="banner banner--error">Cancelled.</p> }
    }
  `,
})
export class OrderStatusBanner {
  status = input.required<OrderStatus>();
}

go deeper

for a junior

Recall the @switch / @case / @default syntax and that there is no break.

for a middle

Explain strict equality, first-match-wins, grouped cases and the parser's rules on what the block may contain.

for a senior

Show migration awareness: NgSwitch rendered every matching case and used == before v17, and grouped cases keep their view.

for a principal

Decide where status-to-view mapping belongs: in the template switch or in a component-level mapping that templates just read.

## Basic shape `@switch` chooses one of several templates by value. For an order-status banner: ```html @switch (status()) { @case ('pending') { <p class="banner">Waiting for payment.</p> } @case ('paid') @case ('packed') { <p class="banner">We are preparing your order.</p> } @case ('shipped') { <p class="banner">On its way.</p> } @default { <p class="banner banner--muted">Status unavailable.</p> } } ``` ## How matching works The compiler turns the block into one expression that stores the switch value in a temporary and compares it with each case in order. In effect: `(tmp = status()) === 'pending' ? caseA : tmp === 'paid' || tmp === 'packed' ? caseB : ...`. That gives the following rules: 1. **The switch expression is evaluated once** per change detection pass, however many cases there are. 2. **Comparison is strict equality (`===`)**. No type coercion: the number `1` does not match `'1'`, and two different objects with the same fields do not match each other. 3. **The first match wins.** Later cases with the same value are never shown. 4. **No fallthrough.** Unlike a JavaScript `switch`, there is no `break`, and one case never runs into the next. 5. **`@default` is optional.** If nothing matches and there is no `@default`, nothing renders. ## Sharing a body between values Because there is no fallthrough, the way to map several values to one body is to write **consecutive `@case` blocks**, putting the body on the last one. The earlier ones have no braces. This was added in **v21.1**. Before that, you duplicated the body, or moved the grouping into the component (for example a `computed()` that maps `'paid'` and `'packed'` to a single `'preparing'` value). Grouped cases also share **one view**: if the status moves from `'paid'` to `'packed'`, the chosen template does not change, so Angular keeps the existing view instead of destroying and recreating it. Moving to a different group swaps the view. ## What the parser enforces | Rule | Error when broken | |---|---| | `@switch` has exactly one parameter | `@switch block must have exactly one parameter` | | Only `@case` and `@default` inside | `@switch block can only contain @case and @default blocks` | | Each `@case` has exactly one parameter | `@case block must have exactly one parameter` | | At most one `@default`, with no parameters | `@switch block can only have one @default block` | Whitespace and HTML comments between the cases are allowed, so formatting is free, but a heading or wrapper element placed directly inside `@switch` is not; put it inside a case or outside the block. ## Switching on a signal Most modern components hold the status in a signal or a signal input, so the switch reads `@switch (status())`. The call happens once per change detection pass, inside the template's reactive context, so the view is marked for refresh whenever the signal changes, including in an `OnPush` component. Two limits come with the call form: TypeScript does not narrow the result of a call, so a case body cannot rely on the switch to narrow an object read through the signal, and the exhaustiveness check described below cannot see through it. When either matters, read the value into a template variable first with `@let`, then switch on that variable. For a plain string union where the case bodies only render static markup, `@switch (status())` is perfectly fine. ## Choosing `@switch` over `@if` - Use `@switch` when **one value** selects among **several fixed alternatives**, such as an enum or a string union. - Use an `@if` / `@else if` chain when the branches test **different conditions** (`isAdmin()`, `hasDraft()`), or ranges (`total() > 100`). - For a union type, `@switch` also supports **exhaustiveness checking** with `@default never;`, which an `@if` chain does not. ## Compared with `NgSwitch` The deprecated `[ngSwitch]` directive also matches with `===` (it used `==` before v17, which is a migration trap for older code), but each `*ngSwitchCase` is an independent directive, so **every** matching case renders, not just the first, and multiple `*ngSwitchDefault` elements all render together. `@switch` has JavaScript-like semantics: exactly one branch, or none.

  • In Angular, why does @case (1) never match when the status comes from a query string?
    Query parameters are strings, so the switch value is `'1'`, and `@switch` compares with `===`, which does not coerce types. Convert the value in the component, for example with `numberAttribute` or `Number()`, or write the cases as strings.
  • Does an Angular @switch recreate its view when the value moves between two grouped @case values?
    No. Consecutive `@case` blocks share one template, so the selected template index stays the same and Angular keeps the existing view. The view is only swapped when the value moves to a case with a different body.

saying these in an interview costs you the question

  • @switch cases fall through unless you add a break
  • @case values are compared with loose equality ==
  • Every matching @case renders, as with NgSwitch
  • A heading element can sit directly inside @switch
  • @default is required in every @switch