'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!
Solution 1:[1]
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:[2]
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:[3]
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 4:[4]
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 5:[5]
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 6:[6]
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.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | |
| Solution 3 | |
| Solution 4 | |
| Solution 5 | DG1 |
| Solution 6 | bobbyg603 |
