Introduction
Angular 18.1 arrives with a noteworthy compiler addition: the capacity to declare one or more template variables directly. What does this feature look like in practice, and which scenarios benefit from it most? This article explores those questions.
The compiler's newest capability: @let
Recent Angular releases have consistently expanded the compiler's toolkit through what's known as the @-syntax. This pattern previously gave us the modern control flow statements:
- @if
- @for
- @switch
and, most recently, @let.
Traditionally, creating a template variable required either the *ngIf structural directive combined with the as keyword, or the newer control flow equivalent using @if with as.
<!-- older control flow syntax -->
<div *ngIf="user$ |async as user">
{{ user.name }}
</div>
<!-- new control flow syntax -->
@if(user$ |async; as user){
<div>{{ user.name }}</div>
}
This approach was handy for capturing the output of the async pipe into a variable for subsequent template use.
Yet the syntax carries inherent limitations. The surrounding condition evaluates whether the async pipe's result is truthy — meaning it must not equal any JavaScript-falsy value. This works perfectly when the payload is an object or array. Complications arise, however, when the value is numeric, specifically zero.
@if(((numbers$ |async) !=== undefined || (numbers$ |async) !=== null) ; as myNumber){
<div>{{ myNumber }}</div>
}
This is precisely where @let shines.
Unlike @if, @let performs no truthiness check. Its sole purpose is to introduce a local template variable in a straightforward manner. The earlier example becomes considerably cleaner and more readable:
@let myNumber = (numbers$ | async) ?? 0;
<div>{{ myNumber }}</div>
Now the myNumber variable is always rendered, regardless of its value.
The various ways to employ @let
A frequent need when introducing a variable is capturing the outcome of a complex expression. Calling functions directly inside conditions has long been discouraged due to performance concerns — any template event, even a mouse move, would trigger re-evaluation.
With @let, this concern diminishes. As mentioned, it declares a local variable rather than evaluating continuously. The variable is recalculated only when one of its dependencies shifts. Therefore, invoking a function for complex expressions becomes entirely acceptable:
<ul>
@for(user of users(); track user.id) {
@let isAdmin = checkIfAdmin(user);
<li>User is admin: {{ isAdmin }}</li>
}
</ul>
Combining @let with signals
@let integrates seamlessly with signals. The pattern looks like this:
@let userColor = user().preferences?.colors?.primaryColor || 'white';
<span>user favorite color is {{ userColor }}</span>
@let with JavaScript expressions
The versatility of @let extends to nearly any JavaScript expression — except operations like class instantiation. Arithmetic operators are processed normally, and multiple variables can be declared across separate lines or condensed onto a single line.
<div>
@for (score of scores(); track $index) {
@let total = total + score, max = calcMax(score);
<h1>final score: {{ total }}</h1>
}
</div>
Additional advantages of @let
Beyond the fundamentals, @let mirrors JavaScript's let semantics, yielding several practical benefits:
- scoping rules align with JavaScript's
letbehavior - improved type inference throughout the template
- a compilation error surfaces when a variable is referenced prior to its declaration
