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'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.
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@switchare built into the framework - JavaScript-like syntax: conditions and loops read like standard code
- Native
@elsesupport: no moreng-templatereferences 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:
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:
<!-- 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:
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 Expression | Use Case | Performance Impact |
|---|---|---|
track item.id | Items with unique identifiers | Optimal: minimal DOM updates |
track $index | Static lists that never reorder | Acceptable: full re-render on reorder |
track item | Reference tracking | Poor: new reference = new DOM node |
Contextual Variables in @for Blocks
Angular provides implicit variables inside @for blocks that expose iteration metadata:
@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:
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:
@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:
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:
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:
// 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:
<!-- 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:
# Migrate entire project
ng generate @angular/core:control-flow
# Migrate specific directory
ng generate @angular/core:control-flow --path=src/app/featuresThe schematic handles most transformations automatically, but review the output for edge cases:
| Structural Directive | Control 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
trackexpression in@foris mandatory: use unique identifiers likeitem.idfor optimal performance, avoidtrack itemwhich causes reference-based re-renders @ifsupports native@else ifand@elsebranches withoutng-template, and theaskeyword aliases truthy values for reuse@switchuses strict equality and has no fallthrough: consecutive@casestatements 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-flowto migrate existing templates automatically
Can you spot the bug in Angular?
One real snippet, one hidden bug, one attempt a day. No account needed to try.

Written by
Anthony Fillion-MailletFounder of SharpSkill
Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.
Updated on September 3, 2026
Tags
Share
Related articles

Advanced Angular Dependency Injection in 2026: Providers, Tokens and Interview Questions
Master Angular dependency injection with InjectionToken, hierarchical injectors, resolution modifiers, and multi-providers. Includes interview questions and production patterns.

Angular HttpClient and Interceptors in 2026: Request Handling and Interview Questions
Master Angular HttpClient with functional interceptors, authentication patterns, error handling, and caching strategies. Includes common interview questions and real-world code examples.

Angular 19 Interview Questions: Signals, SSR and Must-Know Concepts
The most common Angular 19 interview questions: Signals, incremental hydration, zoneless change detection, and new reactive APIs with code examples and expected answers.