Angular Material and semantic icon identifiers: now with type checking

In previous article I’ve described a way to define meaningful semantic icon identifiers to be used with mat-icon. But that was done on the CSS level, which does not provide much support in terms of syntax/type checking, e.g. wrong icon identifier does not generate warnings or errors, it’s just a blank space in the running application.

Now I’m going to describe a solution which covers TypeScript level (and, by extension, Angular templates too — as long as your editor is aware of Angular Language Service). It does not invalidate CSS approach — they work best together.

So, if you use icon identifier in HTML code, it will be just a string, without any type checking:

<mat-icon fontIcon="menu-dropdown-icon">

And if icon is configured in TypeScript code, it would look like this:

interface MenuItem {
icon: string;
}
// Later in template:<mat-icon [fontIcon]="item.icon">

As a first step, let’s define enum which will hold all icon identifiers:

export enum IconId {
MenuDropdownIcon = 'menu-dropdown-icon';
}

Now we can add types to TypeScript part:

interface MenuItem {
icon: IconId;
}

Still does not help with mat-icon component, and we have no (easily maintainable) way of overriding types of its inputs.

We could wrap whole mat-icon into custom component, but that’s really inconvenient because we’d have to either mirror or abandon lots of functionality baked into it, and that wrapper could break interoperability with other Angular Material components.

But we can defined new attribute and attach it to mat-icon — by implementing custom directive:

@Directive({
selector: 'mat-icon[appIcon]',
})
export class IconDirective {
@Input() set appIcon(value: IconId) {
this.matIcon.fontSet = 'my-icon-set';
this.matIcon.fontIcon = value;
}

constructor(
private readonly matIcon: MatIcon,
) {
}
}

And then it can be used like this:

<mat-icon [appIcon]="IconId.MenuDropdownIcon"></mat-icon>

Of course, to make IconId available in template, it must be exposed by declaring component property:

@Component(...)
export class MyComponent {
...
readonly IconId = IconId; ...
}

Somewhat inconvenient, but a fair price for type checks. One possible workaround is union type like this:

export type IconId
= 'icon-id-1'
| 'icon-id-2'
| 'icon-id-3';

Depending on your IDE or editor, one of the ways can have richer support than the other, e.g. for me WebStorm allows to navigate around enums easily, but not with union type if I’m working with template file. Completion also is not as smooth as it could be. Type-checking, on the other hand, works pretty good.