Discriminated Unions Don't Seem To Work In Angular 9.0.0-next.5 When fullTemplateTypeCheck Is Enabled
As part of the latest version of Angular 9.0.0-next.5 and its CLI (Command-Line Interface), the tsconfig.json
file now contains a compile option called, fullTemplateTypeCheck
. This feature uses TypeScript to validate the Angular expression bindings in your component templates. And, from what I can see, this validation does not appear to support Discriminated Unions. I have attempted to isolate this issue for reproducibility.
I've looked at the Discriminated Union in TypeScript before, in the context of NgRx. But, as a quick refresher, a Discriminated Union allows us to consume collections that contain mixed data-types as long as each data-type has a read-only property that uniquely identifies said type. We can then use guard-statements, like if
or case
, to check the read-only property before referencing the type-specific properties.
This concept will become more clear when we look at the issue in an Angular 9.0.0-next.5 application. To reproduce the issue, I took the following steps:
- I updated my global CLI,
npm i -g @angular/cli@next
. - I created a new app,
ng new ngtest
. - I went into the app,
cd ngtest
. - I added a Discriminated Union to the
AppComponent
. - I consumed the Discriminated Union in the
AppComponent
template. - I tried to build,
npm run ng -- build --prod
.
Here's the code for my AppComponent
class:
import { Component } from '@angular/core';
interface Foo {
type: "foo"; // Discriminated union type identifier.
foo: string;
}
interface Bar {
type: "bar"; // Discriminated union type identifier.
bar: string;
}
interface Baz {
type: "baz"; // Discriminated union type identifier.
baz: string;
}
type Thing = ( Foo | Bar | Baz );
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.less']
})
export class AppComponent {
public values: Thing[];
constructor() {
this.values = [
{
type: "foo", // Discriminated union type identifier.
foo: "This is a foo kind of thing."
},
{
type: "bar", // Discriminated union type identifier.
bar: "This is the bar item."
},
{
type: "baz", // Discriminated union type identifier.
baz: "Baz it to me, baby."
}
];
for ( var value of this.values ) {
// As we loop over the items in the discriminated union, we can use the
// switch / case statement as the "guard". This will implicitly validate the
// lower-level property references based on the guarded type.
switch ( value.type ) {
case "foo":
console.log( value.foo );
break;
case "bar":
console.log( value.bar );
break;
case "baz":
console.log( value.baz );
break;
}
}
}
}
As you can see, my values
collection composes type Thing
, which is a union of the types Foo
, Bar
, and Baz
. Together, these three types make up the discriminated union which differentiates based on the read-only .type
property. When we loop over the values
collection in the constructor, we can switch
on this .type
property in order to safely references type-specific properties within each case
statement.
In the TypeScript code, this works perfectly well. The problem is when we try to do the same thing in the HTML template:
<div *ngFor="let value of values">
<p [ngSwitch]="value.type">
<span *ngSwitchCase="( 'foo' )">
{{ value.foo }}
</span>
<span *ngSwitchCase="( 'bar' )">
{{ value.bar }}
</span>
<span *ngSwitchCase="( 'baz' )">
{{ value.baz }}
</span>
</p>
</div>
As you can see, our Angular component template is doing the same thing that our AppComponent
is doing: it's looping over the values, switching on the .type
property, and then attempting to reference the type-specific properties. However, if we attempt to build this Angular application:
npm run ng -- build --prod
... while the fullTemplateTypeCheck
is enabled, we get the following error:
error TS2339: Property 'foo' does not exist on type 'Foo | Bar | Baz'. Property 'foo' does not exist on type 'Bar'.
error TS2339: Property 'bar' does not exist on type 'Foo | Bar | Baz'. Property 'bar' does not exist on type 'Foo'.
error TS2339: Property 'baz' does not exist on type 'Foo | Bar | Baz'. Property 'baz' does not exist on type 'Foo'.
Now, if we turn off the fullTemplateTypeCheck
property, the Angular application compiles and runs correctly:
npm run ng -- serve --prod --open
Which gives us the following output:
As you can see, the discriminated union is consumed perfectly well in both the component constructor as well as in the component template.
I love the idea of using TypeScript to validate my Angular component templates. But, currently, it seems to be unable to properly interpret the ngSwitch
and ngSwitchCase
directives as "guard statements" that can navigate a discriminated union. And, this is a pattern that I end-up using quite often to render mixed-type collections. So, unless anyone has a suggestion on how to work around this problem, I'll probably have to disable fullTemplateTypeCheck
for the time being.
Want to use code from this post? Check out the license.
Reader Comments
@All,
So, something I just learned is that you can use
$any()
within a Component template to side-step type-checking for a given expression:www.bennadel.com/blog/3736-using-any-to-temporarily-disable-type-checking-within-a-component-template-in-angular-9-0-0-rc-4.htm
This
$any()
directive can be really helpful in cases like this, where the AoT compiler can't properly deduce the correct Type.There's a GitHub issue for this in the Angular repo, and one of the commenters produced a way to use type guards and pipes to keep type safety: https://github.com/angular/angular/issues/34522#issuecomment-762973301
@Lance,
Oh, that's really interesting! I have to admit that I haven't used a "Guard" type before. I've read about it; but, the majority of my TypeScript work has been relatively "simple" in the grand scheme of things. Thanks for linking me to that issue.
I use type guards a lot, but I don't think I would ever have thought of wrapping a type guard in a generic pipe like that, except for seeing the comment.
It works well, but my teammates have asked me to add more explanation in comments in the PR review, so it's not particularly straight-forward.