Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type narrowing for NgSwitch and NgSwitchCase #20780

Open
chuckjaz opened this issue Dec 4, 2017 · 22 comments
Open

Type narrowing for NgSwitch and NgSwitchCase #20780

chuckjaz opened this issue Dec 4, 2017 · 22 comments
Assignees
Labels
area: common Issues related to APIs in the @angular/common package area: compiler Issues related to `ngc`, Angular's template compiler compiler: template type-checking feature: under consideration Feature request for which voting has completed and the request is now under consideration feature Issue that requests a new feature P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent
Milestone

Comments

@chuckjaz
Copy link
Contributor

chuckjaz commented Dec 4, 2017

Current behavior

No type narrowing is performed when using NgSwitch and NgSwitchCase.

Expected behavior

Narrow the types used in the NgSwitchCase based on the selection criteria.

See #17953 (comment) and following for more details.

@chuckjaz chuckjaz added area: core Issues related to the framework runtime feature Issue that requests a new feature labels Dec 4, 2017
@ngbot ngbot bot added this to the Backlog milestone Jan 23, 2018
@dirkluijk
Copy link
Contributor

dirkluijk commented Feb 1, 2018

Looking forward to see type guard support in language-service as well!

My simple use case:

interface Animal {
  type: 'fish' | 'bird';
}

interface Fish extends Animal {
  type: 'fish';
  foo: string;
}

interface Bird extends Animal {
  type: 'bird';
  bar: string;
}

@Component({})
class MyComponent {
  animal: Animal;

  isFish(animal: Animal): animal is Fish {
    return animal.type === 'fish';
  }

  isBird(animal: Animal): animal is Bird {
    return animal.type === 'bird';
  }
}

Use case 1

<app-fish *ngIf="isFish(animal)" [foo]="animal.foo"></app-fish>
<app-bird *ngIf="isBird(animal)" [bar]="animal.bar"></app-bird>

Use case 2

<ng-container [ngSwitch]="true">
  <app-fish *ngSwitchCase="isFish(animal)"></app-fish>
  <app-bird *ngSwitchCase="isBird(animal)"></app-bird>
</ng-container>

Are both use cases covered?

@waterplea
Copy link
Contributor

This would be a great addition, having an issue similar to case 2 that @dirkluijk posted.

@amitport
Copy link
Contributor

@chuckjaz any updates? Anyone working on this? Thanks

@dirkluijk
Copy link
Contributor

Will this be solved in Ivy?

@SchnWalter
Copy link
Contributor

No, this isn't solved by Ivy, if anything, Ivy will cause more people to realize how badly they need ngSwitch to support type narrowing.

@pkozlowski-opensource pkozlowski-opensource added area: common Issues related to APIs in the @angular/common package area: compiler Issues related to `ngc`, Angular's template compiler and removed area: core Issues related to the framework runtime labels Mar 17, 2020
@klemenoslaj
Copy link
Contributor

This would really clean up a lot of template code. How come this is such a low priority?

@Lonli-Lokli
Copy link

It has 63 upvotes so seems like pretty needed feature.

@petebacondarwin petebacondarwin added the P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent label Jan 21, 2021
@ColinLaws
Copy link

ColinLaws commented Feb 18, 2021

I just enabled strict type checking on templates, and I find it to be a highly beneficial feature; however, without type narrowing, you're really limited on the approach you can take when creating a component that accepts polymorphic data as an input, and conditionally render based on the type of data.

@amakhrov
Copy link

amakhrov commented Mar 4, 2021

I ended up updating our existing templates that used ngSwitch to using ngIf - the latter properly supports type narrowing.

@angular-robot angular-robot bot added the feature: under consideration Feature request for which voting has completed and the request is now under consideration label Jun 4, 2021
@simeyla
Copy link

simeyla commented Jun 19, 2021

It may never be possible for ngSwitch to have this kind of behavior.

The good news is you do not need to use a type guard function (isFish(), isBird()) to get type narrowing for *ngIf when using a discriminated union.

I've found this the most convenient way for displaying 'polymorphic' templates. No you won't get the nice 'switch' syntax or a default clause, but it sure is nice to finally see the correct type inferred inside a template.

Here's the updated types, note that Animal is a type here not an interface as shown in OP.

  type Animal = Fish | Bird;
  
  // Fish and Bird can be either interface or type
  // Note : Only Fish has 'fishThing' string
  interface Fish  {
    type: 'fish';
    fishThing: string;
  }
  
  interface Bird  {
    type: 'bird';
    birdThing: string;
  }

In the component:

animal: Animal = { type: 'fish', fishThing: 'fish thing '};

Then in the template if I put animal.type == 'fish' it will automatically assert the type.

                     <div *ngIf="animal.type == 'fish'">{{ animal.fishThing }}</div>
                     <div *ngIf="animal.type == 'fish'">{{ animal.birdThing }}</div>    

This will result in an error for animal.birdThing:

image

This won't work if Animal is an interface because it can't do the type narrowing without a type guard.

@petebacondarwin petebacondarwin added this to Yes but decide effort in Feature Requests Jul 2, 2021
@alxhub alxhub moved this from Yes but decide effort to Needs Project Proposal in Feature Requests Jul 8, 2021
@alxhub alxhub self-assigned this Jul 8, 2021
@maxime1992
Copy link

@alxhub closed the issue above ☝️ saying it was a duplicate but I'm not sure it is so I'll jsut reiterate quickly here:

I often end up using in a switch case the default to call a function which expects a parameter of type never and pass the argument of the switch to it. This way, if there's any case missing in the switch, typescript will throw an error as the remaining type is not of type never.

I think that if we could somehow replicate this behavior within the ngSwitch, that'd be a nice addition

@ValentinFunk
Copy link

The above would be fantastic, I also do this a lot to check for exhaustiveness. In a larger codebase it can be tricky to consider all places that e.g. an enum is used so when you extend logic it is super helpful to have the compiler tell you where you missed something (error at compile time). E.g.

class UnreachableCaseError extends Error {
	constructor(val: never) {
		super(`Unreachable case: ${val}`);
	}
}

function getColorName(c: Color): string {
    switch(c) {
        case Color.Red:
            return 'red';
        case Color.Green:
            return 'green';
        default:
            throw new UnreachableCaseError(c);
    }
}

It would be fantastic to have this in templates, too, so that we never end up with a piece of data that cannot be rendered

@JoostK JoostK moved this from Needs Project Proposal to Proposed Projects in Feature Requests Oct 12, 2021
@Harpush
Copy link

Harpush commented Sep 25, 2022

Any news concerning this issue? Currently support for ngSwitch and else type narrow is the only thing that hold us back from enabling strict templates :(

@Lonli-Lokli
Copy link

Is there possibility with new typescript satisfied operator?
https://devblogs.microsoft.com/typescript/announcing-typescript-4-9-beta/#the-satisfies-operator

@scr2em
Copy link

scr2em commented Feb 7, 2023

Come on it's 2023

@MickL
Copy link

MickL commented Mar 14, 2023

5 years later, still not implemented :(

Using functions as @dirkluijk commented is not an option for me as this functions will run each time the change detection runs and their pure purpose is to prevent type checking errors on build.

@Lonli-Lokli
Copy link

I think this issue is not closed based on the ngswitch implementation, it just requires rewriting

@flevi29
Copy link

flevi29 commented Jun 16, 2023

5 years later, still not implemented :(

Using functions as @dirkluijk commented is not an option for me as this functions will run each time the change detection runs and their pure purpose is to prevent type checking errors on build.

I've been wondering if this is true, switch would have to re-run each time too, it needs to check which template to render. So a nested "if-else-if" ugly depressing monstrosity should be just as good. Of course there's some added overhead but it should be absolutely minimal. The biggest headache is the resulting monster lasagna code. It's so much more verbose and hard to read than it has to be.

@Jrubzjeknf
Copy link

Check discussion #50719, header switch block .

#switch has several major benefits over NgSwitch:

It does not require a container element to hold the condition expression or each conditional template.
It can support template type-checking, including type narrowing within each branch.

@stealthAngel
Copy link

stealthAngel commented Aug 25, 2023

Although I like the answer of @dirkluijk There is a much simpler solution:

export type ShapeType = 'circle' | 'square' | 'rectangle';

export interface Shape {
  type: ShapeType;
}

export interface Circle extends Shape {
  radius: number;
}

export interface Square extends Shape {
  sideLength: number;
}

export interface Rectangle extends Shape {
  width: number;
  height: number;
}
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  json: string = `[{
    "type": "circle",
    "radius": 11
  },
  {
    "type": "square",
    "sideLength": 15
  },
  {
    "type": "rectangle",
    "height": 10,
    "width": 20
  }
  ]`;

  shapes: Shape[] = JSON.parse(json);
}
<ng-container *ngFor="let shape of shapes" [ngSwitch]="shape.type">
  <app-circle *ngSwitchCase="'circle'" [shape]="shape"></app-circle>
  <app-rectangle *ngSwitchCase="'rectangle'" [shape]="shape"></app-rectangle>
  <app-square *ngSwitchCase="'square'" [shape]="shape"></app-square>
  <div *ngSwitchDefault>Onbekend</div>
</ng-container>
@Component({
  selector: 'app-square',
  templateUrl: './square.component.html',
  styleUrls: ['./square.component.css']
})
export class SquareComponent {
  @Input()
  public shape: Shape | undefined;

  protected get square(): Square {
    return this.shape as Square;
  }

  constructor() { }
}

In the component i'm casting it to the shape.

see my git repository for an example:

@baratgabor
Copy link

One technique that can help:

  // In component.ts
  // This type forces to create an object that always has exactly the same members & values as the original string union.
  animalType: { [K in Animal['type']]: K } = {
    bird: 'bird',
    fish: 'fish'
  }
<ng-container [ngSwitch]="animal.type">
  <app-fish *ngSwitchCase="animalType.bird"></app-fish>
  <app-bird *ngSwitchCase="animalType.fish"></app-bird>
</ng-container>

@maxime1992
Copy link

@baratgabor this is about NgSwitch and NgSwitchCase. Nothing will be enforced on the template side as far as I know with your code above

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: common Issues related to APIs in the @angular/common package area: compiler Issues related to `ngc`, Angular's template compiler compiler: template type-checking feature: under consideration Feature request for which voting has completed and the request is now under consideration feature Issue that requests a new feature P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent
Projects
Feature Requests
Proposed Projects
Development

No branches or pull requests