Plog
Plog

Reputation: 9622

Type casting within a template in Angular 2

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!

Upvotes: 92

Views: 80343

Answers (9)

Muhammet Can TONBUL
Muhammet Can TONBUL

Reputation: 3538

If you don't care about type control.

In Angular 8 and higher versions

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

From Offical Document: https://angular.dev/tools/cli/template-typecheck

New Document: https://angular.dev/tools/cli/template-typecheck

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

Upvotes: 101

smnbbrv
smnbbrv

Reputation: 24531

This pipe can be used to take the type from various inputs. It works pretty well with classes, named types / interfaces and primitives.

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 / variable.

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: {{ (propNumber | as : 123).toFixed(2) }}
</td>

Requires strict templates and Ivy.

Upvotes: 42

Arun Saini
Arun Saini

Reputation: 7804

This is still not supported by Angular. You can create a custom pipe or a function to typecast.

Or you can cast it to 'any' using the following syntax: $any() .

Example:

{{$any(person).address.street}}

Reference doc: https://angular.io/guide/template-typecheck

Upvotes: 0

Simone Nigro
Simone Nigro

Reputation: 4887

Disclaimer! I'm the author of ng-as Angular library with pipe and directive for type casting template variables.

Casting with directirve eg.:

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

// your interface, but also work with any typescript type (class, type, etc.)
interface Person {
  name: string;
}

@Component({
  selector: 'app-root',
  template: `
  <ng-container *ngTemplateOutlet="personTemplate; context: {$implicit: person}"></ng-container>
  <ng-template #personTemplate [ngAs]="Person" let-person>
    <span>Hello {{ person.name }}!</span>
  </ng-template>
  `,
})
export class AppComponent {
  // NOTE: If you have "strictPropertyInitialization" enabled, 
  // you will need to add a non-null assertion (!)
  public Person!: Person; // publish your interface into html template
  person: Person = { name: 'Simone' }; // the data
}

Casting with pipe eg.:

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

// your interface, but also work with any typescript type (class, type, etc.)
interface Person {
  name: string;
}

@Component({
  selector: 'app-root',
  template: `
  <ng-container *ngTemplateOutlet="personTemplate; context: {$implicit: person}"></ng-container>
  <ng-template #personTemplate let-person>
    <span>Hello {{ (person | as: Person).name }}!</span>
  </ng-template>
  `,
})
export class AppComponent {
  // NOTE: If you have "strictPropertyInitialization" enabled, 
  // you will need to add a non-null assertion (!)
  public Person!: Person; // publish your interface into html template
  person: Person = { name: 'Simone' }; // the data
}

source of pipe:

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

@Pipe({ name: 'as', pure: true })
export class NgAsPipe implements PipeTransform {
  // eslint-disable-next-line no-unused-vars
  transform<T>(input: unknown, baseItem: T | undefined): T {
    return input as unknown as T;
  }
}

source of directive:

import { Directive, Input } from "@angular/core";

interface NgAsContext<T> {
  ngLet: T;
  $implicit: T;
}

@Directive({ selector: '[ngAs]' })
export class NgAsDirective<T> {
  @Input() ngAs!: T;

  static ngTemplateContextGuard<T>(dir: NgAsDirective<T>, ctx: any): ctx is NgAsContext<Exclude<T, false | 0 | '' | null | undefined>> {
      return true;
  }
}

More info on: https://www.npmjs.com/package/ng-as

Upvotes: 1

Khaled Lela
Khaled Lela

Reputation: 8119

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

Upvotes: 9

bobbyg603
bobbyg603

Reputation: 3840

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.

Upvotes: 6

DG1
DG1

Reputation: 79

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>

Upvotes: 6

Qortex
Qortex

Reputation: 7456

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.

Upvotes: 29

G&#252;nter Z&#246;chbauer
G&#252;nter Z&#246;chbauer

Reputation: 657078

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"

Upvotes: 50

Related Questions