Elegant type narrowing with @let syntax in Angular

Wojciech Trawiński
JavaScript everyday
3 min readMay 19, 2024
Photo by Karan Chawla on Unsplash

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! 🙂

--

--