skip to content

Conditional Blocks

@if with @else if, @else and an as alias, and @switch with @case and @default, including narrowing and exhaustive checks. Interviewers ask how they replace the deprecated *ngIf.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In current Angular templates, how do you write an if / else-if / else, and how does it replace *ngIf with an else template?

level: juniorimportance: must knowfreq 78%

answer

  1. built into the template syntax
  2. blocks, not a directive
  3. no ng-template reference needed
  4. deprecated since v20

basics

~20 s

Use the built-in @if (cond) { } @else if (other) { } @else { } blocks. They replace *ngIf="cond; else tpl" plus an <ng-template #tpl>, need no import, and *ngIf has been deprecated since v20.

solid answer

~40 s

Since v17 Angular templates have built-in control flow. `@if (condition) { ... }` renders its content when the condition is truthy, any number of `@else if (other) { ... }` branches can follow, and a single `@else { ... }` must come last. The old form, `*ngIf="loaded; else loading"` with a separate `<ng-template #loading>`, needed the `NgIf` directive imported and split one decision across two places in the template; there was also no `else if` at all. The blocks are part of the template language, so a standalone component imports nothing to use them, and the compiler checks the branches as one construct. `NgIf` is deprecated since v20 with intent to remove, so new code uses `@if`, and existing templates are migrated.

code

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

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

@Component({
  selector: 'app-order-banner',
  template: `
    @if (status() === 'cancelled') {
      <p class="banner banner--error">This order was cancelled.</p>
    } @else if (status() === 'shipped') {
      <p class="banner">On its way.</p>
    } @else {
      <p class="banner">We are preparing your order.</p>
    }
  `,
})
export class OrderBanner {
  status = input.required<OrderStatus>();
}

go deeper

for a junior

Recall the @if / @else if / @else syntax and translate a *ngIf with an else template into it.

for a middle

Explain what the parser enforces and that the chosen branch's view is created and destroyed as the condition changes.

for a senior

Discuss migrating large templates off *ngIf, removing leftover imports, and where @if destroying state is a bug.

for a principal

Plan when to schedule control-flow migration against other upgrade work, given NgIf is deprecated but still supported.

## The built-in conditional block Angular v17 added **control flow blocks** to the template language. The conditional family is: ```html @if (order.status === 'cancelled') { <p class="banner banner--error">This order was cancelled.</p> } @else if (order.status === 'shipped') { <p class="banner">On its way.</p> } @else { <p class="banner">We are preparing your order.</p> } ``` The rules the parser enforces: - `@if` takes **one expression** in parentheses, optionally followed by an `as` alias. - **Any number** of `@else if` blocks may follow. - **At most one** `@else`, and it must be **last**; it takes no parameters. - A branch renders when its expression is **truthy** by JavaScript rules; the first truthy branch wins and the rest are skipped. Because the blocks are syntax, not directives, there is nothing to import. A standalone component can use `@if` with an empty `imports` array. ## What it replaces Before v17 the same template needed the `NgIf` structural directive: ```html <p *ngIf="order.status === 'cancelled'; else notCancelled">Cancelled.</p> <ng-template #notCancelled> <p *ngIf="order.status === 'shipped'; else preparing">On its way.</p> </ng-template> <ng-template #preparing> <p>We are preparing your order.</p> </ng-template> ``` | Concern | `*ngIf` | `@if` | |---|---|---| | Else branch | a `#ref` to an `<ng-template>` placed elsewhere | an `@else { }` block right after | | Else-if | nest another `*ngIf` inside the else template | `@else if (...) { }` | | Imports | `NgIf` or `CommonModule` in the component | none | | Multiple root nodes | wrap in `<ng-container *ngIf>` | put them straight in the block | | Status | deprecated since v20, intended for removal | current recommendation | The `*` form is shorthand for wrapping the element in an `<ng-template>`, and the else branch has to be a named template because a directive can only receive other templates through inputs. The block syntax removes that indirection: the branches sit next to each other in reading order. ## What happens at runtime The compiler turns the whole `@if` chain into one instruction that evaluates the conditions in order during change detection and returns the index of the branch to show (or none). Then: 1. If the **chosen branch changed**, Angular destroys the previous branch's view and creates the new one. 2. If the **same branch** is still chosen, the existing view is kept and simply updated. 3. If **no branch** matches and there is no `@else`, nothing is rendered. Destroying a branch destroys its components and directives and runs their cleanup, the same as removing any view. That is intended: an `@if` is for showing different content, not for hiding content that should keep its state. ## Narrowing inside the branches Under strict template type checking, the compiler reproduces the chain as real TypeScript `if` statements, so a branch body is type-checked with the condition's narrowing applied. `@if (order.shipment) { {{ order.shipment.carrier }} }` compiles without a possibly-undefined error, because `order.shipment` is a property path TypeScript can narrow. Each `@else if` and the `@else` also see the **negation** of the earlier conditions, so an `@else` after `@if (status === 'cancelled')` knows the status is not `'cancelled'`. Narrowing does not apply to calls: `@if (order()) { {{ order().id }} }` still sees `order()` as possibly empty, which is where the `as` alias or an `@let` variable helps. The same guards are applied to event handlers inside the branch, so a `(click)` in the branch body is checked with the narrowed type too. ## Common mistakes - Putting `@else` before an `@else if`: the parser reports that the `@else` block must be last. - Writing two `@else` blocks: only one is allowed. - Expecting `@if` to hide content while preserving component state; for that, bind a class or the `hidden` attribute instead. - Keeping `NgIf` or `CommonModule` in `imports` after migrating; under strict templates the `unusedStandaloneImports` diagnostic (NG8113) can flag an import the template no longer uses. ## When the old syntax still appears Most production codebases still contain `*ngIf`, and it keeps working in v22 as a deprecated API. Interviewers often ask candidates to read both forms, so it is worth being able to translate one into the other in both directions, including `*ngIf="value$ | async as value"`, which becomes `@if (value$ | async; as value)`.

  • In Angular, what happens to a component inside an @if branch when the condition becomes false?
    The branch's view is destroyed, so the component is destroyed and its cleanup runs. When the condition becomes true again, a fresh instance is created with fresh state. If the state must survive, hide the content with a class or the `hidden` attribute, or keep the state in a parent or a service.
  • How do you write *ngIf="user$ | async as user; else loading" with Angular's built-in control flow?
    `@if (user$ | async; as user) { ... } @else { ... }`, moving the loading markup from the `<ng-template #loading>` into the `@else` block. Note the semicolon before `as` in the block form.

saying these in an interview costs you the question

  • @if requires importing NgIf or CommonModule
  • @else can appear anywhere among the @else if branches
  • @if only toggles CSS visibility and keeps the DOM
  • *ngIf supported else-if chains directly
  • *ngIf was removed in v17 when @if arrived
open as a page

In Angular's @if block, what does the as alias hold, and why does @if (unreadCount(); as count) hide the badge when there are zero messages?

level: middleimportance: should knowfreq 45%

basics

~20 s

The as alias holds the value of the @if expression, evaluated once per check. The block still renders only when that value is truthy, so a count of 0 hides it; test count !== null instead, or show the number outside the @if.

open as a page

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%

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).

open as a page

When migrating Angular templates from *ngIf and NgSwitch to @if and @switch, which behaviour differences can change what renders?

level: seniorimportance: should knowfreq 40%

basics

~20 s

NgSwitch renders every matching case and every default, while @switch renders one branch, and pre-v17 NgSwitch matched with ==. *ngIf's host element moves inside the @if block, and a shared else template cannot simply be inlined.

open as a page

In an Angular order-status banner, how do you make the build fail when a new OrderStatus value has no @case, and why does @switch (order().status) defeat it?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

End the @switch with @default never; so template type checking reports any unhandled union member. It relies on TypeScript narrowing, which does not work on a call like order().status, so read the signal into @let first.

open as a page