angular: Discriminated union types not resolved properly with ngSwitch
š 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
}
}
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Reactions: 64
- Comments: 32 (8 by maintainers)
The solution to this problem is to use
*ngIf
with 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
Rectangle
type guard)This is something we would really love to see fixed. We use discriminated unions very heavily in our code, which also means we canāt turn on the strict type checking yet, even though it would provide a tremendous value for us.
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ā¦
This is a big challenge, mainly as itās a design limitation with how
NgSwitch
works. I think we should look into ways of doing this, but realistically I donāt think this is something we can address quickly.@jaufgang thanks for this easy and reusable solution. I adapted it a little bit so it also works for unknown data as well.
https://angular-guard-type-pipe-ahwgyp.stackblitz.io
Closing as this is now a solved problem with the new control flow syntax.
Any chances this annoying bug is going to be fixed any time soon?
Introducing workarounds as features is not a good idea either. Thereās also other issues, however, that pitch the idea of bringing types into the templates in some way.
In any case, type-casting like that is already possible in a slightly more verbose way (consider replacing ngIf with something that can deal with falsy values). Just be aware that at the end of the day this is still just a lie to the compiler and not bullet-proof.
Personally I use this ātrickā everytime I need to type the context variables of an ng-template which would otherwise just be
any
.Maybe some way to cast values in templates could be a workaround?
Of course right now we can use $any:
As long as it is as simple as this example this works, although it completely looses type-checks. However when the element contains many references to the type in question, the template becomes quickly littered with
$any
-calls. Some way to cast for a whole element would reduce this.@jaufgang, yep, that fixed it. Thanks for looking into it.
I am familiar with the advisory against invoking methods or getters directly in templates, though I am somewhat skeptical that it translates to poor performance here, given that the type guard method is so lightweight. I did a quick performance audit in my browser and didnāt observe a meaningful difference between the two approaches (your āpure pipeā vs directly calling the guard).
In any case, thanks for putting the work into this workaround. Hopefully the underlying issue gets an actual fix soon.
FYI , running
npm install -S @angular/compiler@11.0.9
in the downloaded stackblitz was enough to get it to build cleanly for me.@Lonli-Lokli Yeah, there is https://github.com/angular/angular/issues/28731
Iāve created a stackblitz to report this issue, and only after that I was able to find this issue thatās already open. Anyway, just in case it helps, hereās my stackblitz: https://stackblitz.com/edit/angular-issue-ngswitch-type-narrowing
This issue is quite annoying, so I really hope that it gets enough attention from the Angular team and gets fixed soon.
This is not what we want to represent. instead, we want to have a subtype of Animal that satisfies the constraint
type: 'some-type'
.You can see example implementation in the playground
I also created gist with usage example https://gist.github.com/amakhrov/a7b2a30b13668f714d40e095db437486
Hope this helps.
This is an actually really annoying issue which happens with any GraphQL generated schema. It also prevents my team from using strict template checking.
We hope this is addressed soon.
Our workaround for the time being (instead of disabling template checking completely) is to set the type of these values to
any
. Is there any better solution to apply until this is resolved?@grantwhiting Nope, this one hasnāt been figured out yet. I donāt see this being modeled using the currently available type checking tools, as
ngSwitchCase
expressions have to be combined with the correspondingngSwitch
case during type checking codegen, so I think there is a need for some custom codegen logic.