Skip to main content
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Mike Henke
Ben Nadel at cf.Objective() 2010 (Minneapolis, MN) with: Mike Henke ( @mikehenke )

Discriminated Unions Don't Seem To Work In Angular 9.0.0-next.5 When fullTemplateTypeCheck Is Enabled

By on

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:

  1. I updated my global CLI, npm i -g @angular/cli@next.
  2. I created a new app, ng new ngtest.
  3. I went into the app, cd ngtest.
  4. I added a Discriminated Union to the AppComponent.
  5. I consumed the Discriminated Union in the AppComponent template.
  6. 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:

Discriminated Union consumed fine in Angular template when template checking is disabled.

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

15,663 Comments

@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.

2 Comments

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.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel