In Angular templates, what does an @let declaration do, why can it not be reassigned, and where is it visible?
answer
- a named template expression
- re-evaluated like a binding
- no assignment in handlers
- view and descendants only
- not hoisted
basics
~20 sAngular's @let name = expression; gives a template expression a name that stays in sync like a binding. It cannot be assigned to, and it is visible after its declaration in its own view and nested views, not in parents or siblings.
solid answer
~50 s`@let` (developer preview in 18.1, stable since v19) declares one template-local name for an expression: `@let total = price() * qty();`. Angular re-evaluates it as part of the view's update, like a binding, so it always reflects the current expression; that is why assigning to it, as in `(click)="total = 0"`, is a compile error (`Cannot assign to @let declaration`). It is visible from its declaration onward in the view that declares it and every view nested inside, but it is not hoisted: reading it earlier is an error, and a parent or sibling block cannot see one declared inside an `@if` or `@for`. A plain element does not create a view, so an `@let` inside a `<div>` is still visible after the `<div>`. Typical uses are naming a repeated computation and unwrapping an `async` pipe once.
code
ts · 18 linesimport {Component, input} from '@angular/core';
import {CurrencyPipe} from '@angular/common';
@Component({
selector: 'app-price-tag',
imports: [CurrencyPipe],
template: `
@let discounted = price() * (1 - discountRate());
<span class="was">{{ price() | currency }}</span>
@if (discounted < price()) {
<strong>{{ discounted | currency }}</strong>
}
`,
})
export class PriceTag {
readonly price = input.required<number>();
readonly discountRate = input(0);
}go deeper
Recall the syntax, that it ends with a semicolon, that it is read-only, and one real use such as naming a discounted price.
Explain that @let is re-evaluated like a binding, why that forbids assignment, and the scoping table: later in the view and nested views only.
Spot shadowing of component members, the read-before-definition error, and when a computed() in the class is the better home.
Agree on a team boundary: @let for naming and unwrapping in templates, computed() for anything with business meaning or tests.
## What @let is `@let` is template syntax for naming the result of an **Angular template expression**: ```html @let subtotal = price() * quantity(); @let discountAmount = subtotal * discountRate(); @let total = subtotal - discountAmount; <p>Subtotal: {{ subtotal | currency }}</p> @if (discountAmount > 0) { <p>You save {{ discountAmount | currency }}</p> } <p>Total: {{ total | currency }}</p> ``` It arrived as a developer preview in Angular 18.1 and has been stable since v19. The formal shape is `@let`, whitespace, a name, `=`, an expression that may span lines, and a terminating `;`. Each `@let` declares **exactly one** name; comma-separated lists are not allowed. The value can be any template expression, including pipes (`@let user = user$ | async;`), object and array literals, and signal reads. ## Why it is read-only A `@let` is not a variable you manage; it is a **binding with a name**. Angular re-evaluates it during the view's update pass, so the name always equals the current value of its expression. Allowing writes would create two sources of truth: the expression and whatever the handler assigned. So: - `(click)="total = 0"` fails to compile with `Cannot assign to @let declaration 'total'`; - to change what a `@let` shows, change the state its expression reads, typically a signal in the component; - two-way binding to a `@let` is only accepted when the value is itself a signal, which you then write through its API. ## Where it is visible `@let` follows the **view** scoping of template names, plus a no-hoisting rule: | Location of the read | Visible? | |---|---| | later in the same view | yes | | inside a nested `@if`, `@for`, `@defer` or `<ng-template>` | yes | | after the `<div>` that contains the declaration | yes, elements do not create views | | earlier in the same view, before the declaration | no: `Cannot read @let declaration '...' before it has been defined` | | in the parent of the block that declares it | no | | in a sibling block, such as the `@else` of the `@if` that declares it | no | Other rules the compiler enforces: 1. A `@let` cannot share a name with another template symbol in the **same** scope, such as a `#ref`, and reports a conflicting-declaration error. 2. An inner view may declare its own `@let` with the same name as an outer one; the inner one takes precedence there. 3. A `@let` that is declared but never read is flagged by the `unusedLetDeclaration` extended diagnostic. ## Name versus component member Template names **shadow** component members with the same name. If the component has a `total` signal and the template declares `@let total = ...`, every later `total` in that scope means the `@let`. To read the member explicitly, write `this.total()`. That matters inside the declaration itself: `@let total = total();` reads the `@let` before it is defined and is an error, whereas `@let total = this.total();` reads the component's signal. ## Where @let helps, and where it does not Good uses: - **naming a computation used several times**, as in the discount display above; - **unwrapping an observable once** with `async` and reusing the value in many bindings; - **shortening a long path** such as `@let address = order().customer.shippingAddress;`. Things to keep in the class instead: - logic that needs tests or is reused across templates belongs in a `computed()`; - `@let` is not state: it cannot be incremented, toggled or set from a handler.
- Is an Angular @let evaluated once when the view is created, or kept up to date?Kept up to date. Angular evaluates a `@let` as part of the view's update pass, the same way it evaluates bindings, so its value always follows the expression. That is why it is read-only: assigning to it would fight the next evaluation.
- What does @let user = user(); do in an Angular template whose component has a user signal?It is a compile error: inside its own initializer, `user` resolves to the `@let` being declared, which is read before it has been defined. Writing `@let user = this.user();` reads the component's signal explicitly and works; from then on, `user` in that scope means the `@let`.
saying these in an interview costs you the question
- @let computes its value once when the view is created
- You can update a @let from a click handler like a JavaScript let
- @let is hoisted, so it can be read anywhere in the template
- A @let inside a <div> is invisible after the closing </div>
- One @let can declare several names separated by commas