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

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

Closed
michaelbromley opened this issue Jul 6, 2017 · 11 comments
Closed
Labels
area: language-service Issues related to Angular's VS Code language service type: bug/fix

Comments

@michaelbromley
Copy link

michaelbromley commented Jul 6, 2017

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
@chuckjaz
Copy link
Contributor

chuckjaz commented Jul 7, 2017

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.

@chuckjaz chuckjaz closed this as completed Jul 7, 2017
@michaelbromley
Copy link
Author

michaelbromley commented Jul 9, 2017

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.

@chuckjaz
Copy link
Contributor

chuckjaz commented Jul 10, 2017

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

@albv
Copy link

albv commented Dec 4, 2017

@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;
  }
}

@chuckjaz
Copy link
Contributor

chuckjaz commented Dec 4, 2017

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.

@chuckjaz
Copy link
Contributor

chuckjaz commented Dec 4, 2017

I added #20780 to track this.

@albv
Copy link

albv commented Dec 5, 2017

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

@crutchcorn
Copy link

crutchcorn commented May 1, 2018

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

@istvandesign
Copy link

istvandesign commented Oct 3, 2018

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.

@simeyla
Copy link

simeyla commented Dec 11, 2018

@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

@angular-automatic-lock-bot
Copy link

angular-automatic-lock-bot bot commented Sep 14, 2019

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.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 14, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: language-service Issues related to Angular's VS Code language service type: bug/fix
Projects
None yet
Development

No branches or pull requests

7 participants