Skip to content

Language Service: More forgiving support of union types in templates #17953

Closed

Description

@michaelbromley

I'm submitting a ...


[ ] Regression (behavior that used to work and stopped working in a new release)
[] Bug report 
[x] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

When I have a variable in my component with a union type, the language service will complain if I reference any properties which are not common to all members of the union type. Sometimes it may be valid to try to reference these non-shared properties, e.g.:

import { Component } from '@angular/core';

export interface Line {
  length: number;
  dimensions: number;
}

export interface Plane {
  area: number;
  dimensions: number;
}

@Component({
  selector: 'app-root',
  template: ` {{ shape.length || shape.area }}
              {{ shape.dimensions }}

              <div *ngFor="let item of collection">
                  {{ item.length || item.area }}
                  {{ item.dimensions }}
              </div>
`})
export class AppComponent {
  collection: Array<Line|Plane> = [];
  shape: Line|Plane;
}

Here is a screenshot of the output from VS Code using the language service plugin:

language-service-error

Expected behavior

I would expect to be able to use the above construction without errors.

Minimal reproduction of the problem with instructions

  1. Create a new angular CLI project with @angular/cli 1.2.0
  2. Replace the contents of app.component.ts with the code above
  3. View in VS Code with the language service plugin enabled. My version of the plugin uses @angular/language-service@4.1.0-beta

What is the motivation / use case for changing the behavior?

The example above illustrates a simple uses case. In my app I have a situation where I have a generic component which can be passed different types of object, and uses the || operator to display the first truthy value.

Please tell us about your environment

 
For Tooling issues:
- Node version: 7.7.1
- Platform: Windows 10
- VS Code 1.13.1
- @angular/language-service@4.1.0-beta
- Typescript@2.1.5

Activity

  1. chuckjaz commented on Jul 7, 2017

    @chuckjaz
    Contributor

    This is equivalent to this playground example in which TypeScript also reports an error.

    As we improve type checking during AOT compilation this will be reported as an error in 5.0 as well as we will use TypeScript's type checker instead of our own.

  2. michaelbromley commented on Jul 9, 2017

    @michaelbromley
    Author

    Thanks for the reply. The difference is that in regular TS code I can cast before accessing the property, which I cannot do in the template.

    const v = (item as Line).length || (item as Plane).area;

    I guess the way to do this now would be then to provide a method on the component which performs the cast and returns the appropriate value, and then just call this method in the template.

  3. chuckjaz commented on Jul 10, 2017

    @chuckjaz
    Contributor

    That is certainly what we recommend. The templates expressions should be simple and any complicated logic should be in the component.

  4. albv commented on Dec 4, 2017

    @albv

    @chuckjaz and what about tagged union types? For example:

    interface PayPal {
        type: "paypal";
        email: string;
    }
    
    interface CreditCard {
        type: "creditcard";
        cardNumber: string;
        securityCode: string;
    }
    
    type PaymentMethod = PayPal | CreditCard;
    
    @Component({
      selector: 'my-payment-method',
      template: `
        <div [ngSwitch]="paymentMethod.type">
          <div *ngSwitchCase="'paypal'">{{ paymentMethod.email }}</div>
          <div *ngSwitchCase="'creditcard'">{{ paymentMethod.cardNumber }}</div>
        </div>
      `
    })
    export class PaymentMethodComponent {
      @Input()
      paymentMethod: PaymentMethod;
    }
    

    Should we do type casting in component also? Like this:

    @Component({
      selector: 'my-payment-method',
      template: `
        <div [ngSwitch]="paymentMethod.type">
          <div *ngSwitchCase="'paypal'">{{ paymentMethodAsPaypal.email }}</div>
          <div *ngSwitchCase="'creditcard'">{{ paymentMethodAsCreditCard.cardNumber }}</div>
        </div>
      `
    })
    export class PaymentMethodComponent {
      @Input()
      paymentMethod: PaymentMethod;
    
      get paymentMethodAsPaypal(): PayPal() {
        return this.paymentMethod as PayPal;
      }
    
      get paymentMethodAsCreditCard(): CreditCard() {
        return this.paymentMethod as CreditCard;
      }
    }
    
  5. chuckjaz commented on Dec 4, 2017

    @chuckjaz
    Contributor

    Interesting problem. For this to work I need some way to declare the type relation between NgSwitch.ngSwitch and NgSwitchCase.ngSwitchCase. For this I would need to add a type parameters to NgSwitch and NgSwitchCase such that it would look like:

    export class NgSwitch<T> {
      ...
      @Input()
      set ngSwitch(newValue: T} {
        ...
      }
      ...
    }
    
    export class NgSwitchCase<T> {
       ...
      @Input() ngSwtichCase: T;
    }
    

    Following the pattern of #20702, I would then need to declare a guard for ngSwitch which is a bit odd since it the guard would need to be declared in NgSwitchCase, but, ignoring that for a bit, the guard might look something like,

      ...
      static ngSwitch_member_ngSwitchCaseTypeGuard = <T, K extends string, V extends S>(a: T, k: K, v: V) => a is T & {[T in K]: V};
      ...
    

    which would then allow me to generate the following type-check block:

    if (NgSwitchGuard.ngSwitch_member_ngSwitchCaseTypeGuard(component.paymentMethod, "type", "paypal") {
      component.paymentMethod.email;
    }

    A simplified version of this can be seen in the playground here.

    I currently can only get this to work for the string union types and deciding when and how to apply it is a bit tricky in general (the odd part ignored above) but this seems like we might eventually be able to do this.

    Unfortunately, adding a type parameter to NgSwitch is a breaking change that will not be able to land until 6 but I probably would not get to this before 6 anyway so that is not blocker.

  6. chuckjaz commented on Dec 4, 2017

    @chuckjaz
    Contributor

    I added #20780 to track this.

  7. albv commented on Dec 5, 2017

    @albv

    Looking forward to see it landed. Thanks for the detailed response @chuckjaz

  8. crutchcorn commented on May 1, 2018

    @crutchcorn
    Contributor

    This does not seem to be fixed still. Type unions are still not working as expected in templates

  9. istvandesign commented on Oct 3, 2018

    @istvandesign

    error: number | ErrorType = <ErrorType>{}; will cause template errors in vscode with Angular 6. Also vscode seems to be much slower for some reason with the template open.

  10. simeyla commented on Dec 11, 2018

    @simeyla

    @albv Your code looks so freakishly like mine I actually thought it was mine for a second.
    That is to say I'm working on a similar problem with a list of shopping cart payment methods.

    I modified your answer slightly, to what I think makes a little more sense if you don't want to change the object being 'switched' upon. In your case you added paymentMethodAsPayPal() to the class (component) that contains paymentMethod.

    In my case, I have an array of pure (from JSON) data representing a list of payment methods, and couldn't / didn't want to add methods onto it. Instead I created the following method to use in the component:

    Component class

    // convert a Braintree 'VaultedMethod' (interface) into a 'VaultedCreditCard'
    public asCreditCard(method: VaultedMethod) {
        return method as VaultedCreditCard;
    }
    

    Component template

      <ng-container [ngSwitch]="method.type">
    
             <div *ngSwitchCase="'CreditCard'">
    
                  <strong>Ends in {{ asCreditCard(method).accountNumber }}</strong></span>  
                  <span>{{ asCreditCard(method).cardType }}</span>
                            
             </div>
    
     </ng-container>
    

    This ends up looking more like a 'cast' within a block where you've clearly established the data type.

    If you really think people can make a mistake you could do a check inside asCreditCard to make sure it really was the correct type

  11. angular-automatic-lock-bot commented on Sep 14, 2019

    @angular-automatic-lock-bot

    This issue has been automatically locked due to inactivity.
    Please file a new issue if you are encountering a similar or related problem.

    Read more about our automatic conversation locking policy.

    This action has been performed automatically by a bot.

  12. locked and limited conversation to collaborators on Sep 14, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions