instanceof in Angular HTML templates

Vasily Ivanov
5 min readNov 20, 2022

The instanceof type guard is widely used in TypeScript because it helps to resolve two typical problems at the same time. In many different cases developers need to check if a class is of a specific type; also, it is often useful to narrow the type of the variable that is checked. The instanceof operator brilliantly does both things in TypeScript code, however it might be important to do the same in Angular HTML templates. Unfortunately, the Angular framework does not provide developers with a standard tool that does the job. Nevertheless, there is a way to create a substitute covering most use cases.

To provide you with a good illustration of the used approach, I have made a minimal example that is located at https://github.com/VasilyIvanov/angular-instanceof-pipe-anf-switch and also at https://stackblitz.com/edit/angular-ivy-az5nlu. It was created and tested with Angular 12 and 14. When developing, I used Angular public repositories https://github.com/angular/angular/blob/main/packages/common/src/directives/ng_switch.ts#L110 and https://github.com/angular/angular/blob/main/packages/common/src/directives/ng_if.ts.

PIPE

The first Angular object that comes to mind when we try to approach the problem is pipe. In fact, the main purpose of pipes is transforming data in order to display. However, they might also be used in directive parameters which makes it possible to apply the ngIf directive and, in turn, assign the result to a template variable. The pipe is simple and straightforward, a possible implementation is given below.

import { Pipe } from '@angular/core';
import type { PipeTransform } from '@angular/core';

type AbstractType<T> = abstract new (...args: any[]) => T;

@Pipe({
name: 'instanceof',
pure: true,
})
export class InstanceofPipe implements PipeTransform {
public transform<V, R>(value: V, type: AbstractType<R>): R | undefined {
return value instanceof type ? value : undefined;
}
}

Here we declare a type AbstractType<T> to take a class constructor function as a parameter. There is also a standard type Type<T> in the framework but it will not accept abstract classes, so the new declaration is desirable.

To test the pipe, we can write some simple code. First, let us create a few test classes, A, B, C, and D. A is an abstract class. B and C are inherited from A. D is not abstract and not inherited from any other class.

export abstract class A {
public constructor(public readonly valueA: string) {}
}
import { A } from "./a";

export class B extends A {
public constructor(public readonly valueA: string, public readonly valueB: string) {
super(valueA);
}
}
import { A } from "./a";

export class C extends A {
public constructor(public readonly valueA: string, public readonly valueC: string) {
super(valueA);
}
}
export class D {
public constructor(public readonly valueD: string) {}
}

After declaring the pipe in the in AppModule, let us add some test data to the app.component.ts file.

  public readonly list: (A | D)[] = [
new B('testA1', 'testB1'),
new C('testA2', 'testC1'),
new B('testA3', 'testB2'),
new C('testA4', 'testC2'),
new B('testA5', 'testB3'),
new D('testD1'),
];

Normally, in HTML templates we cannot directly access class constructor function. To cope with this problem, we can simply declare public class variables in the .ts file.

import { A } from './a';
import { B } from './b';
import { C } from './c';
import { D } from './d';
...
public readonly A = A;
public readonly B = B;
public readonly C = C;

Now we are ready to write some test code in the HTML template, i.e. in the app.component.html file.

<ng-container *ngFor="let item of list">
<p *ngIf="item | instanceof: B as b" class="b">
I'm B with values '{{ b.valueA }}' and '{{ b.valueB }}'.
<ng-container *ngIf="item | instanceof: A">I'm also A.</ng-container>
</p>
<p *ngIf="item | instanceof: C as c" class="c">
I'm C with values '{{ c.valueA }}' and '{{ c.valueC }}'.
<ng-container *ngIf="item | instanceof: A">I'm also A.</ng-container>
</p>
</ng-container>

Finally, we can add some visual effects to the .css file.

.b {
color: red;
}

.c {
color: blue;
}

If you run the application (ng serve), you can see that the pipe works as expected. You should see the following messages on your screen.

I’m B with values ‘testA1’ and ‘testB1’. I’m also A.

I’m C with values ‘testA2’ and ‘testC1’. I’m also A.

I’m B with values ‘testA3’ and ‘testB2’. I’m also A.

I’m C with values ‘testA4’ and ‘testC2’. I’m also A.

I’m B with values ‘testA5’ and ‘testB3’. I’m also A.

SWITCH

The solution works correctly and is very compact, however sometimes we need more. In the example we have used only 4 classes. But what if we had 20? In this case 20 ngIf directives would be added which might negatively affect performance. In such use cases in many programming languages, we would use the switch-case construction. Fortunately, the Angular team has created a similar option for HTML templates. We cannot directly apply the ngSwitch, ngSwitchCase, and ngSwitchDefault directives, however we can look into their code and alter it in order to resolve the problem.

I cannot include all code in this article especially since it mostly does the same as the original directives do and you can find it in my repository at https://github.com/VasilyIvanov/angular-instanceof-pipe-anf-switch. I will show only some important differences.

First, we need a Context class that will represent the instanceofSwitchCase directive context. This is important because we need to narrow the type that is checked.

class Context<T = unknown> {
public $implicit: T = null!;
public instanceofSwitchCase: T = null!;
}

The ngSwitch directive becomes instanceofSwitch and its _matchCase method is updated.

  /** @internal */
public _matchCase<T>(type: AbstractType<T>): T | undefined {
const matched = this._instanceofSwitch instanceof type ? this._instanceofSwitch : undefined;
this._lastCasesMatched = this._lastCasesMatched || !!matched;
this._lastCaseCheckIndex++;
if (this._lastCaseCheckIndex === this._caseCount) {
this._updateDefaultCases(!this._lastCasesMatched);
this._lastCaseCheckIndex = 0;
this._lastCasesMatched = false;
}
return matched;
}

The instanceofSwitchCase directive will take AbstractType as an input parameter.

  @Input() public instanceofSwitchCase!: AbstractType<T>;

We also need a context guard here because we want to allow the user to declare a template variable.

  public static ngTemplateContextGuard<T>(
dir: InstanceofSwitchCaseDirective<T>,
ctx: any
): ctx is Context<Exclude<T, false | 0 | '' | null | undefined>> {
return true;
}

The instanceofSwitchDefault is not seriously changed comparing to the original ngDefaultCase directive.

After registering all 3 directives in AppModule, we can finally test them in the HTML template.

<ng-container *ngFor="let item of list; let i = index">
<ng-container [instanceofSwitch]="item">
<div *instanceofSwitchCase="A as a">{{ i }}. {{ a.valueA }}</div>
<div *instanceofSwitchCase="B; let b">{{ i }}. {{ b.valueB }}</div>
<div *instanceofSwitchCase="C as c">{{ i }}. {{ c.valueC }}</div>
<div *instanceofSwitchDefault>{{ i }}. DEFAULT</div>
</ng-container>
</ng-container>

You can make sure that the directives do the job when you start the app. As you can see, you can declare template variables in 2 different ways, using ‘as’ or using ‘let’, there is no difference. On the screen you will see the following result.

0. testA1

0. testB1

1. testA2

1. testC1

2. testA3

2. testB2

3. testA4

3. testC2

4. testA5

4. testB3

5. DEFAULT

CONCLUSION

The Angular framework it incredible flexible and provides developers with many opportunities for customisation. The fact that the framework is open-source guaranties that you can find a lot of good examples how to do what you need. This might be useful if you take care of performance and try to avoid easy but potentially slow solution. I hope that this article is a good demonstration of what can be done to efficiently resolve a problem you may encounter in your work.

--

--