skip to content

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%

answer

  1. evaluated once per check
  2. the value, not a boolean
  3. the block still tests truthiness
  4. zero and empty string are falsy

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.

solid answer

~50 s

`@if (expr; as name)` evaluates `expr` once per change detection pass, uses the result as the condition, and exposes the same result inside the block as `name`. It is a convenience for naming a long or costly expression, and for signals it gives a narrowed local (`unreadCount()` returning `number | null` becomes `count: number` inside). The condition is still ordinary JavaScript truthiness, so `0`, `''`, `NaN`, `false`, `null` and `undefined` all skip the block. That is why a badge written as `@if (unreadCount(); as count)` disappears exactly when the count is zero. If zero is a real value, make the condition explicit: `@if (unreadCount() !== null)` and read the count again, or use `@let count = unreadCount();` and test `count !== null`. The alias is only available inside its own block, not in `@else`; `as` is allowed on `@if` and, since v20.2, on `@else if`.

code

ts · 20 lines
ts
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-inbox-badge',
  template: `
    <!-- hides at 0: only right if 0 means 'nothing to show' -->
    @if (unreadCount(); as count) {
      <span class="badge">{{ count }}</span>
    }

    <!-- shows 0 once loaded; null still means 'not loaded' -->
    @let loaded = unreadCount();
    @if (loaded !== null) {
      <span class="badge" [class.badge--empty]="loaded === 0">{{ loaded }}</span>
    }
  `,
})
export class InboxBadge {
  unreadCount = signal<number | null>(null);
}

go deeper

for a junior

Recall that as names the condition's value inside the block, and that the block still needs a truthy value.

for a middle

Explain truthiness with the falsy table, the single evaluation, and the narrowing benefit for signals.

for a senior

Spot count, price and empty-string bugs in review and fix them with explicit null checks or @let.

for a principal

Encourage modelling loading states explicitly (null vs value) so templates never lean on truthiness for meaning.

## What `as` does The `@if` block accepts an optional alias after its condition: ```html @if (order().shipment?.trackingNumber; as tracking) { <a [href]="'/track/' + tracking">Track parcel {{ tracking }}</a> } ``` The compiler evaluates the expression **once** per change detection pass, stores the result, uses it to decide whether the block renders, and passes the same stored value into the block as `tracking`. Two practical benefits: - **Readability and cost**: a long path or a signal read is written once instead of repeated inside the block. - **Type narrowing**: the alias has the expression's type minus the falsy part, so `string | undefined` becomes `string` inside the block. This matters for signals, because a call such as `order()` is not narrowed by TypeScript when repeated, while a named alias is. The parser rules: only one `as` per branch; allowed on `@if` and, since v20.2, on `@else if`; not allowed on `@else`; the name must be a valid identifier. The alias is scoped to its own branch, so an `@else` block cannot read it. ## Why zero disappears The alias does **not** change the condition. The block renders when the value is **truthy** by JavaScript rules, and the following are falsy: | Value | Renders? | Typical source | |---|---|---| | `0` | no | counts, indexes, prices | | `''` | no | empty text fields | | `NaN` | no | failed number parsing | | `false` | no | flags | | `null` / `undefined` | no | not loaded yet | | `[]`, `{}` | yes | empty arrays and objects are truthy | So this badge vanishes exactly when the user has no unread messages: ```html @if (unreadCount(); as count) { <span class="badge">{{ count }}</span> } ``` That is correct if the intent is "show the badge only when there is something unread". It is a bug if the intent is "show the badge once the count has loaded", where `0` is a legitimate value and only `null` means "not loaded". ## Fixes when zero is meaningful 1. **Make the condition explicit** and read the value again inside: `@if (unreadCount() !== null) { {{ unreadCount() }} }`. 2. **Name the value first with `@let`**, then test it: `@let count = unreadCount();` followed by `@if (count !== null) { {{ count }} }`. The `@let` variable keeps the single evaluation and narrows in the block. 3. **Move the number outside the condition** when only the styling should change: always render the badge and bind a class for the empty state. The same trap applies to an `@if` over an empty string (`@if (searchTerm(); as term)` hides the block when the field is cleared) and to the empty-array case in the other direction: `@if (items(); as list)` still renders for `[]`, so "no results" needs `list.length === 0` or an `@empty` block on a loop. ## How the alias behaves while the block stays visible If the expression changes from one truthy value to another (the count goes from 3 to 4), Angular keeps the existing view and only updates the alias. The block is destroyed only when the value becomes falsy, and recreated when it becomes truthy again. Components inside therefore keep their state as long as the value stays truthy, whatever it changes to. ## Choosing the right form | Need | Write | Why | |---|---|---| | Show a block only when a value is present and truthy, and use it inside | `@if (value(); as v)` | one read, narrowed alias | | Treat `0` or `''` as real values | `@if (value() !== null)` or `@let` + explicit test | truthiness would hide them | | Use the value in both the `@if` and the `@else` | `@let v = value();` before the block | the alias is not visible in `@else` | | Only branch, never use the value | `@if (isReady())` | no alias needed | The common thread: `as` is a naming tool, not a presence test. When the meaning of "present" is anything other than JavaScript truthiness, write the test out. ## The same rule in the old syntax `*ngIf="unreadCount$ | async as count"` has exactly the same truthiness behaviour, so this bug is often carried over during a migration rather than introduced by it. The block form just makes it easier to see, because the condition and the alias sit side by side.

  • Can the @else branch of an Angular @if read the as alias?
    No. The alias is scoped to the branch that declares it, and the `@else` branch only runs when that value was falsy, so there is nothing useful to read. If both branches need the value, declare it with `@let` before the `@if`.
  • Why is an as alias useful when the @if condition reads a signal in Angular?
    Repeated calls such as `user()` are not narrowed by TypeScript, so `user().name` inside the block may still be flagged as possibly null. The alias is a single local value that the type checker narrows to the truthy type, and the signal is read once per check.

saying these in an interview costs you the question

  • The as alias makes @if render for any non-null value, including 0
  • The alias is available in the @else branch too
  • An empty array makes @if skip its block
  • The alias re-creates the block every time its value changes
  • *ngIf with as treats zero differently from @if