在角度2的模板中类型铸造

我正在从事一个 Angular 项目(Angular 4.0.0) ,我在将一个抽象类的属性绑定到 ngModel 上遇到了麻烦,因为我首先需要将它强制转换为它实际上是的具体类,以便访问该属性。

也就是说,我有一个 AbstractEvent 类,它有一个具体的实现 Event,它有一个布尔属性“已确认”,我需要通过 ngModel 进行双向绑定,并使用复选框进行设置。

当前我的 DOM 中有这个元素:

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

不幸的是,这会导致以下错误:

未捕获错误: 模板解析错误: 解析器错误: 在[(事件为事件)。确认]中的第8列缺少预期)

在谷歌上搜索似乎表明这可能是因为在模板中使用“ as”时不支持使用它?虽然我不太确定。

我也不知道如何在驱动模板的打印脚本文件中为它编写一个函数,因为这会破坏我需要的 ngModel 上的双向绑定。

如果任何人有任何方法绕过这个或执行类型铸造角模板正确我将非常感激!

46468 次浏览

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"

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

更好的方法是使用管道,这样可以两全其美。只需定义铸管:

@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.

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

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

Requires strict templates and Ivy.

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

    用法:

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

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.

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