Elegant type narrowing with @let syntax in Angular
Angular 18 hasn’t been released yet, however there’s already a new, powerful addition awaiting in the 18.1 version , namely template local variables a.k.a. @let syntax.
In short, it will be possible to define auxiliary local variables in a template in the same way as in JavaScript files.
One of the possible applications is unwrapping values from streams subscribed to using the AsyncPipe:
current solutions
<ng-container *ngIf="data$ | async as data">
<p>{{ data }}</p>
</ng-container>
<ng-container *ngIf="{ data: data$ | async } as vm">
<p>{{ vm.data }}</p>
</ng-container>
new solution
@let data = data$ | async ;
<p>{{ data }}</p>
It also enables creating ‘dynamic’ view model properties, which is especially useful when dealing with collections, e.g., an isActive
property that changes based on user selection.
You can read more about different usages of the new syntax in this great article by Enea Jahollari
What’s worth mentioning is the ability to store a signal’s value in a template local variable which is crucial for type narrowing.
Let’s consider the following component:
type AnalysisState =
| { status: 'pending' }
| { status: 'completed'; result: string };
@Component({
selector: 'app-analysis-card',
standalone: true,
templateUrl: './analysis-card.component.html',
styleUrl: './analysis-card.component.scss',
})
export class AnalysisCardComponent {
public analysisState: AnalysisState = { status: 'pending' };
constructor() {
// mock change simulation
setTimeout(() => {
this.analysisState = { status: 'completed', result: 'xyz' };
}, 5000);
}
}
@if (analysisState.status === "pending") {
<p>Analysis is pending</p>
} @else {
<p>Analysis completed with result: {{ analysisState.result }}</p>
}
The new control flow syntax allows for proper type narrowing, which was not possible with the *ngIf directive. You can read more about it in one of my previous articles
However, if you use signals for handling component’s state:
@Component({
selector: 'app-analysis-card',
standalone: true,
templateUrl: './analysis-card.component.html',
styleUrl: './analysis-card.component.scss',
})
export class AnalysisCardComponent {
public analysisState: WritableSignal<AnalysisState> = signal({
status: 'pending',
});
constructor() {
// mock change simulation
setTimeout(() => {
this.analysisState.set({ status: 'completed', result: 'xyz' });
}, 5000);
}
}
type narrowing is no longer working:
@if (analysisState().status === "pending") {
<p>Analysis is pending</p>
} @else {
<!-- Property 'result' does not exist on type 'AnalysisState'. -->
<p>Analysis completed with result: {{ analysisState().result }}</p>
}
There are two workarounds:
- use getter with signal’s value
@Component({
selector: 'app-analysis-card',
standalone: true,
templateUrl: './analysis-card.component.html',
styleUrl: './analysis-card.component.scss',
})
export class AnalysisCardComponent {
public _analysisState: WritableSignal<AnalysisState> = signal({
status: 'pending',
});
get analysisState() {
return this._analysisState();
}
constructor() {
// mock change simulation
setTimeout(() => {
this._analysisState.set({ status: 'completed', result: 'xyz' });
}, 5000);
}
}
@if (analysisState.status === "pending") {
<p>Analysis is pending</p>
} @else {
<p>Analysis completed with result: {{ analysisState.result }}</p>
}
- unwrap signal’s value with auxiliary
if
block
@Component({
selector: 'app-analysis-card',
standalone: true,
templateUrl: './analysis-card.component.html',
styleUrl: './analysis-card.component.scss',
})
export class AnalysisCardComponent {
public analysisState: WritableSignal<AnalysisState> = signal({
status: 'pending',
});
constructor() {
// mock change simulation
setTimeout(() => {
this.analysisState.set({ status: 'completed', result: 'xyz' });
}, 5000);
}
}
@if (analysisState(); as analysisState) {
@if (analysisState.status === "pending") {
<p>Analysis is pending</p>
} @else {
<p>Analysis completed with result: {{ analysisState.result }}</p>
}
}
Having introduced the new way to create template local variables, there’s a far more elegant way to accomplish the goal:
@let analysisState = _analysisState();
@if (analysisState.status === "pending") {
<p>Analysis is pending</p>
} @else {
<p>Analysis completed with result: {{ analysisState.result }}</p>
}
The @let syntax seems to be a powerful addition, however it should be used in a responsible way so that templates do not get overloaded with logic that should belong to component’s class.
I hope you liked my story, thanks for reading! 🙂