Discriminated union types not resolved properly with ngSwitch
See original GitHub issue🐞 bug report
Affected Package
@angular/compiler
@angular/language-service
Is this a regression?
No
Description
In templates, type resolution fails when using ngSwitch on the discriminant property of a discriminated union type; in all cases, the type seems to be resolved to the first option.
🔬 Minimal Reproduction
The following example uses an enum value as the discriminant, but the same behaviour is seen when using string values as the discriminant.
See https://github.com/jinbijin/minimal-example for the full repository; it’s a new Angular project except for:
app.component.ts:
import { Component } from '@angular/core';
enum MaybeType {
Nothing = 0,
Just = 1
}
interface Nothing {
type: MaybeType.Nothing;
}
interface Just<T> {
type: MaybeType.Just;
value: T;
}
type Maybe<T> = Nothing | Just<T>;
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
MaybeType = MaybeType;
maybeText: Maybe<string> = { type: MaybeType.Just, value: 'hello!' };
}
app.component.html:
<ng-container [ngSwitch]="maybeText.type">
<span *ngSwitchCase="MaybeType.Nothing">Nothing</span>
<span *ngSwitchCase="MaybeType.Just">{{ maybeText.value }}</span>
</ng-container>
<router-outlet></router-outlet>
Expected behaviour: Project compiles without any errors. With strict full template type checking the language service doesn’t generate errors.
Actual behaviour: The following error is generated on running ng serve with full template type checking:
ERROR in src/app/app.component.html:3:43 - error TS-992339: Property 'value' does not exist on type 'Maybe<string>'.
Property 'value' does not exist on type 'Nothing'.
3 <span *ngSwitchCase="MaybeType.Just">{{ maybeText.value }}</span>
~~~~~~~~~~~~~~~~
src/app/app.component.ts:21:16
21 templateUrl: './app.component.html',
~~~~~~~~~~~~~~~~~~~~~~
Error occurs in the template of component AppComponent.
With strict template checking, the language service generates the following error:
Identifier 'value' is not defined. 'Maybe<string>' does not contain such a member
🌍 Your Environment
Angular Version:
_ _ ____ _ ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/
Angular CLI: 9.0.0-rc.7
Node: 12.6.0
OS: darwin x64
Angular: 9.0.0-rc.7
... animations, cli, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router
Ivy Workspace: Yes
Package Version
-----------------------------------------------------------
@angular-devkit/architect 0.900.0-rc.7
@angular-devkit/build-angular 0.900.0-rc.7
@angular-devkit/build-optimizer 0.900.0-rc.7
@angular-devkit/build-webpack 0.900.0-rc.7
@angular-devkit/core 9.0.0-rc.7
@angular-devkit/schematics 9.0.0-rc.7
@ngtools/webpack 9.0.0-rc.7
@schematics/angular 9.0.0-rc.7
@schematics/update 0.900.0-rc.7
rxjs 6.5.3
typescript 3.6.4
webpack 4.41.2
Anything else relevant? tsconfig.json:
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "esnext",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2018",
"dom"
]
},
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true,
"strictTemplates": true
}
}
Issue Analytics
- State:
- Created 4 years ago
- Reactions:54
- Comments:26 (7 by maintainers)

Top Related StackOverflow Question
The solution to this problem is to use
*ngIfwith a pipe that evaluates a type guard and returns the original object with a narrowed type if the object passes the type guard and undefined if it doesn’tThis pipe effectively narrows the type without “lying to the compiler”. (it actually performs proper run-time type checking via the type guard function). It will also throw an error if the object being type-checked cannot be narrowed by the type guard (i.e. it is not a member of the union type being narrowed)
Sample usage:
which renders as:
I created a StackBlitz to demonstrate this live in action. See: https://stackblitz.com/edit/angular-guard-type-pipe
(edit: fixed error in
Rectangletype guard)This is really important… The amount of if else in template based on discriminate union that needs those hacks is too much to not fix it…