question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

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:open
  • Created 4 years ago
  • Reactions:54
  • Comments:26 (7 by maintainers)

github_iconTop GitHub Comments

28reactions
jaufgangcommented, Mar 19, 2021

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’t

// guard-type.pipe.ts

export type TypeGuard<A, B extends A> = (a: A) => a is B;

@Pipe({
  name: 'guardType'
})
export class GuardTypePipe implements PipeTransform {

 transform<A, B extends A>(value: A, typeGuard: TypeGuard<A, B>): B | undefined {
    return typeGuard(value) ? value : undefined;
  }

}

This 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:

// shapes.type.ts

import { TypeGuard } from "./guard-type.pipe";

export interface Square {
  kind: "square";
  size: number;
}

export interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}

export interface Circle {
  kind: "circle";
  radius: number;
}

export type Shape = Square | Rectangle | Circle;

export const isSquare: TypeGuard<Shape, Square> = (
  shape: Shape
): shape is Square => shape.kind === "square";

export const isRectangle: TypeGuard<Shape, Rectangle> = (
  shape: Shape
): shape is Rectangle => shape.kind === "rectangle";

export const isCircle: TypeGuard<Shape, Circle> = (
  shape: Shape
): shape is Circle => shape.kind === "circle";
// app.component.ts

import { Component, VERSION } from "@angular/core";
import { isCircle, isRectangle, isSquare, Shape } from "./shapes.type";

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  shapes: Shape[] = [
    {
      kind: "square",
      size: 5
    },
    {
      kind: "circle",
      radius: 10
    },
    {
      kind: "rectangle",
      height: 7,
      width: 9
    },
    {
      kind: "rectangle",
      height: 8.5,
      width: 11
    },
    {
      kind: "circle",
      radius: 8
    }
  ];

  // set the imported type guard functions as component properties
  // so they can be passed by the template to the guardType pipe
  isCircle = isCircle;
  isRectangle = isRectangle;
  isSquare = isSquare;
}

<!-- app.component.html -->
<ul>
  <li *ngFor="let shape of shapes">
    <strong>{{shape.kind}}</strong>:

    <span *ngIf="shape| guardType: isCircle as circle">
      <!-- circle is strongly typed as a Circle -->
      radius = {{circle.radius}}
    </span>

    <span *ngIf="shape| guardType: isSquare as square">
      <!-- square is strongly typed as a Square -->
      size = {{square.size}}
    </span>

    <span *ngIf="shape| guardType: isRectangle as rectangle">
      <!-- rectangle is strongly typed as a Rectangle -->
      width = {{rectangle.width}}, height = {{rectangle.height}}
    </span>
  </li>
</ul>

which renders as: image

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)

13reactions
Harpushcommented, Oct 17, 2020

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…

Read more comments on GitHub >

github_iconTop Results From Across the Web

*ngSwitch Type Errors with Discriminated Unions in Angular ...
Here's a workaround for resolving *ngSwitch type errors in Angular templates and switching on a discriminated union type.
Read more >
Discriminated Unions Don't Seem To Work In Angular 9.0.0 ...
Ben Nadel demonstrates that the fullTemplateTypeCheck compile option in Angular 9.0.0-next.5 does not appear to be able to use the ngSwitch ...
Read more >
Typescript discriminated union type not recognized
This means things like switch (action.type) statements leveraging discriminated unions stop working with the current example-app patterns.
Read more >
ngLanceFinney
Fortunately, with enough #TypeScript (including type guards and Angular Pipes), ... Discriminated union types not resolved properly with ngSwitch · Issue ...
Read more >
Creative Uses of TypeScript Discriminated Unions
I'm going to do this by illustrating how these techniques addressed a problem that I was trying to solve and then talk about...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found