Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: Claude Englebert
Ben Nadel at Scotch On The Rock (SOTR) 2010 (London) with: Claude Englebert@cfemea )

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

By Ben Nadel 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.



Reader Comments

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.