Angular Control Flow Syntax in 2026: @if, @for, @switch and Interview Questions

Master Angular's built-in control flow syntax with @if, @for, and @switch blocks. Learn the track expression, contextual variables, and common interview questions about Angular 22 template syntax.

Angular Control Flow Syntax @if @for @switch template blocks

Angular's control flow syntax (@if, @for, @switch) replaces the structural directives *ngIf, *ngFor, and *ngSwitch that dominated Angular templates for years. Introduced in Angular 17, stable since Angular 18, and the default in Angular 22, this block-based syntax brings templates closer to standard programming constructs while enabling better performance optimizations.

Migration available

Run ng generate @angular/core:control-flow to automatically migrate existing templates from structural directives to the new control flow syntax. Both syntaxes coexist during migration, but ngIf, ngFor, and ngSwitch are soft-deprecated since Angular 19.

Why Angular Replaced Structural Directives with Block Syntax

Structural directives required importing CommonModule, used microsyntax that differed from JavaScript, and forced developers to wrap content in ng-template for complex conditions. The new control flow syntax solves these problems:

  • No imports required: @if, @for, and @switch are built into the framework
  • JavaScript-like syntax: conditions and loops read like standard code
  • Native @else support: no more ng-template references for else branches
  • Better tree-shaking: unused control flow blocks have zero bundle impact

The Angular documentation recommends fully adopting block syntax in all new projects, and migrating existing codebases incrementally.

@if: Conditional Rendering Without ng-template

The @if block conditionally renders content based on a truthy expression. Unlike *ngIf, it supports @else if and @else branches directly:

user-status.component.tstypescript
import { Component, input } from '@angular/core';

@Component({
  selector: 'app-user-status',
  template: `
    @if (user().role === 'admin') {
      <app-admin-panel />
    } @else if (user().role === 'editor') {
      <app-editor-panel />
    } @else {
      <app-viewer-panel />
    }
  `
})
export class UserStatusComponent {
  user = input.required<{ role: string }>();
}

The @if block also supports variable aliasing with the as keyword, which extracts values from nested expressions:

html
<!-- Avoids repeated property access -->
@if (user().profile?.settings?.theme; as theme) {
  <p>Current theme: {{ theme }}</p>
}

This pattern is particularly useful when working with async data or deeply nested objects, since the aliased variable is only defined when the condition is truthy.

@for: Iteration with Mandatory Track Expression

The @for block iterates over any JavaScript iterable, with optimizations for arrays. Unlike *ngFor, it requires a track expression:

product-list.component.tstypescript
import { Component, input } from '@angular/core';

interface Product {
  id: string;
  name: string;
  price: number;
}

@Component({
  selector: 'app-product-list',
  template: `
    @for (product of products(); track product.id) {
      <app-product-card [product]="product" />
    } @empty {
      <p>No products available</p>
    }
  `
})
export class ProductListComponent {
  products = input.required<Product[]>();
}

The track expression tells Angular how to identify each item across re-renders. Choosing the right tracking property directly impacts performance:

Track ExpressionUse CasePerformance Impact
track item.idItems with unique identifiersOptimal: minimal DOM updates
track $indexStatic lists that never reorderAcceptable: full re-render on reorder
track itemReference trackingPoor: new reference = new DOM node

Contextual Variables in @for Blocks

Angular provides implicit variables inside @for blocks that expose iteration metadata:

html
@for (item of items(); track item.id; let i = $index, isLast = $last) {
  <li class="item" [class.last]="isLast">
    {{ i + 1 }}. {{ item.name }}
  </li>
}

Available contextual variables:

  • $index: zero-based position
  • $count: total number of items
  • $first, $last: boolean flags for first/last items
  • $even, $odd: boolean flags based on index parity

Ready to ace your Angular interviews?

Practice with our interactive simulators, flashcards, and technical tests.

@switch: Type-Safe Conditional Branching

The @switch block provides exhaustive pattern matching with strict equality (===) comparison:

status-badge.component.tstypescript
import { Component, input } from '@angular/core';

type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered';

@Component({
  selector: 'app-status-badge',
  template: `
    @switch (status()) {
      @case ('pending') {
        <span class="badge badge-gray">Pending</span>
      }
      @case ('processing') {
        <span class="badge badge-blue">Processing</span>
      }
      @case ('shipped') {
        <span class="badge badge-yellow">Shipped</span>
      }
      @case ('delivered') {
        <span class="badge badge-green">Delivered</span>
      }
    }
  `
})
export class StatusBadgeComponent {
  status = input.required<OrderStatus>();
}

Unlike JavaScript's switch, Angular's @switch has no fallthrough behavior. Multiple conditions targeting the same block require consecutive @case statements:

html
@switch (userRole()) {
  @case ('admin')
  @case ('superadmin') {
    <app-admin-dashboard />
  }
  @case ('editor')
  @case ('reviewer') {
    <app-editor-dashboard />
  }
  @default {
    <app-viewer-dashboard />
  }
}

Exhaustive Type Checking with @default never

Angular 22 supports compile-time exhaustiveness checking. Using @default never; declares that no remaining cases should exist:

typescript
type Theme = 'light' | 'dark' | 'system';

// Compile error if a Theme value is missing from @case blocks
@switch (theme()) {
  @case ('light') { /* ... */ }
  @case ('dark') { /* ... */ }
  @case ('system') { /* ... */ }
  @default never;
}

Adding a new value to the Theme union triggers a compilation error, forcing developers to handle all cases.

Control Flow Syntax and Angular Signals Integration

Control flow blocks integrate seamlessly with Angular Signals, enabling fine-grained reactivity:

dashboard.component.tstypescript
import { Component, computed, signal } from '@angular/core';

@Component({
  selector: 'app-dashboard',
  template: `
    @if (isLoading()) {
      <app-skeleton />
    } @else if (hasError()) {
      <app-error [message]="errorMessage()" />
    } @else {
      @for (item of filteredItems(); track item.id) {
        <app-item-card [item]="item" />
      } @empty {
        <p>No items match your filters</p>
      }
    }
  `
})
export class DashboardComponent {
  items = signal<Item[]>([]);
  filter = signal('');
  isLoading = signal(true);
  hasError = signal(false);
  errorMessage = signal('');

  // Computed signal recalculates only when dependencies change
  filteredItems = computed(() =>
    this.items().filter(item =>
      item.name.toLowerCase().includes(this.filter().toLowerCase())
    )
  );
}

When a signal updates, Angular re-evaluates only the affected control flow blocks. Combined with Angular's zoneless change detection, this enables highly optimized rendering.

Common Interview Questions About Angular Control Flow

Technical interviews frequently test understanding of control flow syntax, especially the differences from structural directives.

Question 1: Why is track mandatory in @for?

The track expression provides Angular with a stable identity for each item. Without it, Angular cannot efficiently determine which DOM nodes to create, update, or destroy when the collection changes. Making track mandatory forces developers to make an explicit decision about identity, avoiding the performance pitfalls of implicit reference tracking.

Question 2: How does @if differ from *ngIf with async pipe?

Both can handle observables, but @if with signals provides synchronous access to values:

typescript
// With *ngIf and async pipe (legacy)
<div *ngIf="user$ | async as user">{{ user.name }}</div>

// With @if and signals (modern)
@if (user(); as user) {
  <div>{{ user.name }}</div>
}

The signal approach avoids subscription management and integrates better with Angular's change detection.

Question 3: Can @switch replace complex @if/@else if chains?

@switch should be used when comparing a single expression against multiple discrete values. For complex boolean conditions involving different expressions, @if/@else if remains more appropriate:

html
<!-- Use @switch for single expression, multiple values -->
@switch (status()) {
  @case ('active') { ... }
  @case ('inactive') { ... }
}

<!-- Use @if for multiple expressions -->
@if (isAdmin() && hasPermission('write')) {
  ...
} @else if (isEditor()) {
  ...
}

Question 4: What happens to @for when the collection is empty?

When the iterable contains no items, Angular skips the @for content entirely. The optional @empty block renders fallback content in this case. Without @empty, nothing renders, which differs from some frameworks that render an empty container.

Migrating from Structural Directives to Control Flow

The Angular CLI provides an automated migration schematic:

bash
# Migrate entire project
ng generate @angular/core:control-flow

# Migrate specific directory
ng generate @angular/core:control-flow --path=src/app/features

The schematic handles most transformations automatically, but review the output for edge cases:

Structural DirectiveControl Flow Equivalent
*ngIf="condition"@if (condition) { }
*ngIf="condition; else elseBlock"@if (condition) { } @else { }
*ngFor="let item of items"@for (item of items; track item) { }
*ngFor="let item of items; index as i"@for (item of items; track item; let i = $index) { }
[ngSwitch] + *ngSwitchCase@switch + @case

After migration, remove CommonModule imports from standalone components that no longer use structural directives.

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Key Takeaways for Angular Control Flow Syntax

  • Angular's control flow syntax (@if, @for, @switch) is the recommended approach since Angular 19, replacing structural directives
  • The track expression in @for is mandatory: use unique identifiers like item.id for optimal performance, avoid track item which causes reference-based re-renders
  • @if supports native @else if and @else branches without ng-template, and the as keyword aliases truthy values for reuse
  • @switch uses strict equality and has no fallthrough: consecutive @case statements target the same block, and @default never; enables exhaustive type checking
  • Control flow blocks integrate with Signals for fine-grained reactivity: only affected blocks re-render when signals update
  • Run ng generate @angular/core:control-flow to migrate existing templates automatically
Daily challenge

Can you spot the bug in Angular?

One real snippet, one hidden bug, one attempt a day. No account needed to try.

Anthony Fillion-Maillet

Written by

Anthony Fillion-Maillet

Founder of SharpSkill

Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.

Updated on September 3, 2026

Tags

#angular
#control-flow
#templates
#interview

Share

Related articles