Type casting within a template in Angular 2

AngularTypescript

Angular Problem Overview


I'm working on an Angular project (Angular 4.0.0) and I'm having trouble binding a property of an abstract class to ngModel because I first need to cast it as the concrete class it actually is in order to access the property.

i.e. I have an AbstractEvent class this has a a concrete implementation Event which has a boolean property 'acknowledged' which I need a two way binding via ngModel to set with a checkbox.

I currently have this element in my DOM:

<input type="checkbox" *ngIf="event.end" [(ngModel)]="(event as Event).acknowledged" 
                                          [disabled]="(event as Event).acknowledged">

Unfortunately this is throwing the following error:

> Uncaught Error: Template parse errors: Parser Error: Missing expected ) at column 8 in [(event as Event).acknowledged]

Googling around seemed to suggest this might be because using 'as' is not supported when using it inside a template? Although I'm not certain about this.

I also can't work out how to just write a function for it in my typescript file driving the template because this would break the two way binding on ngModel that I require.

If anyone has any way to get around this or perform type casting in angular templates correctly I would be very appreciative!

Angular Solutions


Solution 1 - Angular

If you don't care about type control.

In Angular 8 and higher versions

[(ngModel)]="$any(event).acknowledged"

From Offical Document: https://angular.io/guide/template-typecheck#disabling-type-checking-using-any

@Component({
  selector: 'my-component',
  template: '{{$any(person).addresss.street}}'
})
class MyComponent {
  person?: Person;
}

Solution 2 - Angular

That's not possible because Event can't be referenced from within the template.

(as is also not supported in template binding expressions) You need to make it available first:

class MyComponent {
  EventType = Event;

then this should work

[(ngModel)]="(event as EventType).acknowledged"

update

class MyComponent {
  asEvent(val) : Event { return val; }

then use it as

[(ngModel)]="asEvent(event).acknowledged"

Solution 3 - Angular

As mentioned, using a barebone method call will have performance impact.

A better approach is to use a pipe, and you have best of both worlds. Just define a Cast pipe:

@Pipe({
  name: 'cast',
  pure: true
})
export class CastPipe implements PipeTransform {  
  transform(value: any, args?: any): Event {
    return value;
  }
}

and then in your template, use event | cast when you need the cast.

That way, change detection stays efficient, and typing is safe (given the requested type change is sound of course).

Unfortunately, I don't see a way to have this generic because of the name attribute, so you'd have to define a new pipe for each type.

Solution 4 - Angular

This pipe can be used to take the type from various inputs:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'as',
  pure: true,
})
export class AsPipe implements PipeTransform {

  transform<T>(value: any, _type: (new (...args: any[]) => T) | T): T {
    return value as T;
  }

}

_type argument is unused, but is serving the main goal: the type gets inferred from the constructor.

Could be used as:

class ClassEvent {
  prop: string;
}

interface InterfaceEvent {
  prop: string;
}

export class MyComponent {

  MyClass = ClassEvent; // class constructor

  MyInterface: InterfaceEvent; // typed property

  propString: any; // primitive, string

  propNumber: any; // primitive, number

}
<td mat-cell *matCellDef="let row">
  Type from class constructor: {{ (row | as : MyClass).prop }}
  Type from interface: {{ (row | as : MyInterface).prop }}
  Type from primitive, string: {{ (propString | as : '').substr(1) }}
  Type from primitive, number: {{ (propString | as : 123).toFixed(2) }}
</td>

Requires strict templates and Ivy.

Solution 5 - Angular

  • Using my TypeSafe generics answer:

  • And inspired from smnbbrv answer pass type explicitly as an optional argument when there is nowhere to infer the type from.

     import { Pipe, PipeTransform } from '@angular/core';
     
     /**
      * Cast super type into type using generics
      * Return Type obtained by optional @param type OR assignment type.
      */
    
     @Pipe({ name: 'cast' })
     export class CastPipe implements PipeTransform {
         /**
          * Cast (S: SuperType) into (T: Type) using @Generics.
          * @param value (S: SuperType) obtained from input type.
          * @optional @param type (T CastingType)
          * type?: { new (): T }
          * type?: new () => T
          */
         transform<S, T extends S>(value: S, type?: new () => T): T {
             return <T>value;
         }
     }
    

    Usage:

    template.html

     <input
         type="checkbox"
         *ngIf="event.end"
         [(ngModel)]="(event | cast: Event).acknowledged"
         [disabled]="(event | cast: Event).acknowledged"
     />
    

    component.ts

     export abstract class AbstractEvent {
         end: boolean;
     }
     export class Event extends AbstractEvent {
         acknowledged: boolean;
     }
     
     
     export class MyComponent{
         event: AbstractEvent;
         Event = Event;
     }
    

Solution 6 - Angular

To expand on the answer by @smnbbrv, you can use a similar syntax with interfaces as follows:

@Pipe({ name: 'as', pure: true })
export class AsPipe implements PipeTransform {
  transform<T>(input: unknown, baseItem: T | undefined): T {
    return (input as unknown) as T;
  }
}

This requires us to provide a "baseItem" of the correct type. However, we do not need to actually create the item, we only need to declare it (since the item can be undefined). That means we can create a variable of the suggested type in our class as follows:

export interface Person{
  name: string;
  age: number;
}

export class MyComponent {
  Person: Person;
}

Take note, we're not assigning any value to the baseItem, we're simply specifying its type. If you have strictPropertyInitialization enabled, you will need to add a non-null assertion to your baseItem

export class MyComponent {
  Person!: Person;
}

This can then be used in your template as follows:

<td mat-cell *matCellDef="let row">
  {{ (row | as : Person).name }}
</td>

Solution 7 - Angular

You can also create a function that returns a Type Predicate.

app.component.html

<some-component *ngIf="isFoo(foo)" [foo]="foo"></some-component>

app.component.ts

isFoo(value: Foo | Bar): value is Foo {
    return value === 'Foo';
}

This will cast the template variable foo to type Foo and will silence any strictTemplate errors regarding union types.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionPlogView Question on Stackoverflow
Solution 1 - AngularMuhammet Can TONBULView Answer on Stackoverflow
Solution 2 - AngularGünter ZöchbauerView Answer on Stackoverflow
Solution 3 - AngularQortexView Answer on Stackoverflow
Solution 4 - AngularsmnbbrvView Answer on Stackoverflow
Solution 5 - AngularKhaled LelaView Answer on Stackoverflow
Solution 6 - AngularDG1View Answer on Stackoverflow
Solution 7 - Angularbobbyg603View Answer on Stackoverflow