Angular13 Angular 模板类型检查

2024-02-25 开发教程 Angular13 匿名 5

模板类型检查概述

正如 TypeScript 在代码中捕获类型错误一样,Angular 也会检查应用程序模板中的表达式和绑定,并可以报告所发现的任何类型错误。Angular 当前有三种执行此操作的模式,具体取决于 TypeScript 配置文件 中的 ​fullTemplateTypeCheck ​和 ​strictTemplates ​标志的值。

基本模式

在最基本的类型检查模式下,将 ​fullTemplateTypeCheck ​标志设置为 ​false​,Angular 仅验证模板中的顶层表达式。

如果编写 ​<map [city]="user.address.city">​,则编译器将验证以下内容:

  • user ​是该组件类的属性
  • user ​是具有 ​address ​属性的对象
  • user.address​ 是具有 ​city ​属性的对象

编译器不会验证 ​user.address.city​ 的值是否可赋值给 ​<map>​ 组件的输入属性 ​city​。

编译器在此模式下也有一些主要限制:

  • 重要的是,它不会检查嵌入式视图,比如 ​*ngIf​,​*ngFor​ 和其它 ​<ng-template>​ 嵌入式视图。
  • 它无法弄清 ​#refs​ 的类型、管道的结果、事件绑定中 ​$event​ 的类型等等。

在许多情况下,这些东西最终都以 ​any ​类型结束,这可能导致表达式的后续部分不受检查。

完全模式

如果将 ​fullTemplateTypeCheck ​标志设置为 ​true​,则 Angular 在模板中进行类型检查时会更加主动。特别是:

  • 检查嵌入式视图(比如 ​*ngIf​ 或 ​*ngFor​ 内的 ​*ngFor​)
  • 管道具有正确的返回类型
  • 对指令和管道的本地引用具有正确的类型(any 泛型参数除外,该通用参数将是 ​any​)

以下仍然具有 ​any ​类型。

  • 对 DOM 元素的本地引用。
  • $event​ 对象
  • 安全导航表达式

fullTemplateTypeCheck ​标志已经在 Angular 13 中弃用了。它被编译器选项中的 ​strictTemplates ​家族代替了。

严格模式

Angular 延续了 ​fullTemplateTypeCheck ​标志的行为,并引入了第三个“严格模式”。严格模式是完全模式的超集,可以通过将 ​strictTemplates ​标志设置为 true 来访问。该标志取代 ​fullTemplateTypeCheck ​标志。在严格模式下,Angular 添加了超出 8 版类型检查器的检查。

注意:
严格模式仅在使用 Ivy 时可用。

除了完全模式的行为之外,Angular 版本 9 还会:

  • 验证组件/指令绑定是否可赋值给它们的 ​@Input()
  • 验证以上模式时,会遵守 TypeScript 的 ​strictNullChecks ​标志
  • 推断组件/指令的正确类型,包括泛型
  • 推断配置模板上下文的类型(比如,允许对 ​NgFor ​进行正确的类型检查)
  • 在组件/指令、DOM 和动画事件绑定中推断 ​$event​ 的正确类型
  • 根据标签(tag)名称(比如,​document.createElement​ 将为该标签返回正确的类型),推断出对 DOM 元素的局部引用的正确类型

*ngFor 检查

类型检查的三种模式对嵌入式视图的处理方式不同。考虑以下范例。

interface User {
name: string;
address: {
city: string;
state: string;
}
}
<div *ngFor="let user of users">
<h2>{{config.title}}</h2>
<span>City: {{user.address.city}}</span>
</div>

<h2>​ 和 ​<span>​ 在 ​*ngFor​ 嵌入式视图中。在基本模式下,Angular 不会检查它们中的任何一个。但是,在完全模式下,Angular 会检查 ​config ​和 ​user ​是否存在,并假设为 ​any ​的类型。在严格模式下,Angular 知道该 ​user ​在 ​<span>​ 中是 ​User ​类型,而 ​address ​是与一个对象,它有一个 ​string ​类型的属性 ​city​。

排除模板错误

使用严格模式,你可能会遇到在以前的两种模式下都没有出现过的模板错误。这些错误通常表示模板中的真正类型不匹配,而以前的工具并未捕获这些错误。在这种情况下,该错误消息会使该问题在模板中的位置清晰可见。

当 Angular 库的类型不完整或不正确,或者在以下情况下类型与预期不完全一致时,也可能存在误报。

  • 当库的类型错误或不完整时(比如,如果编写库的时候没有注意 ​strictNullChecks​,则可能缺少 ​null | undefined​)
  • 当库的输入类型太窄并且库没有为 Angular 添加适当的元数据来解决这个问题时。这通常在禁用或使用其它通用布尔输入作为属性时发生,比如 ​<input disabled>​。
  • 在将 ​$event.target​ 用于 DOM 事件时(由于事件冒泡的可能性,DOM 类型中的 ​$event.target​ 不具有你可能期望的类型)

如果发生此类误报,则有以下几种选择:

  • 在某些情况下,使用 ​$any()​ 类型转换函数可以选择不对部分表达式进行类型检查
  • 你可以通过在应用程序的 TypeScript 配置文件 ​tsconfig.json​ 中设置 ​strictTemplates: false​ 来完全禁用严格检查
  • 通过将严格性标志设置为 ​false​,可以在保持其它方面的严格性的同时,单独禁用某些特定的类型检查操作
  • 如果要一起使用 ​strictTemplates ​和 ​strictNullChecks​,则可以通过 ​strictNullInputTypes ​来选择性排除专门用于输入绑定的严格空类型检查

除非另行说明,下面的每个选项都会设置为 ​strictTemplates ​的值(当 ​strictTemplates ​为真时是 ​true​,其他值也一样)。

严格标志

影响

strictInputTypes

是否检查绑定表达式对 `@Input()` 字段的可赋值性。也会影响指令泛型类型的推断。

strictInputAccessModifiers

在把绑定表达式赋值给 `@Input()` 时,是否检查像 `private`/`protected`/`readonly` 这样的访问修饰符。如果禁用,则 `@Input` 上的访问修饰符会被忽略,只进行类型检查。本选项默认为 `false`,即使当 `strictTemplates` 为 `true` 时也一样。

strictNullInputTypes

检查 `@Input()` 绑定时是否要 `strictNullChecks`(对于每个 `strictInputTypes`)。当使用的库不是基于 `strictNullChecks` 构建的时,将其关闭会很有帮助。

strictAttributeTypes

是否检查使用文本属性进行的 @Input()绑定。例如,

<input matInput disabled="true">
(将 disabled属性设置为字符串 'true'
<input matInput [disabled]="true">

(将 disabled属性设置为布尔值 true)。

strictSafeNavigationTypes

是否根据 `user` 的类型正确推断出安全导航操作的返回类型(比如 `user?.name`)。如果禁用,则 `user?.name` 的类型为 `any`。

strictDomLocalRefTypes

对 DOM 元素的本地引用是否将具有正确的类型。如果禁用,对于 `` 来说 `ref` 会是 `any` 类型的。

strictOutputEventTypes

对于绑定到组件/指令 `@Output()` 或动画事件的事件绑定,`$event` 是否具有正确的类型。如果禁用,它将为 `any`。

strictDomEventTypes

对于与 DOM 事件的事件绑定,`$event` 是否具有正确的类型。如果禁用,它将为 `any`。

strictContextGenerics

泛型组件的类型参数是否应该被正确推断(包括泛型上界和下界). 如果禁用它,所有的类型参数都会被当做 `any`。

strictLiteralTypes

是否要推断模板中声明的对象和数组字面量的类型。如果禁用,则此类文字的类型就是 `any`。当 `fullTemplateTypeCheck` 或 `strictTemplates` 为 `true` 时,此标志为 `true`。

如果使用这些标志进行故障排除后仍然存在问题,可以通过禁用 ​strictTemplates ​退回到完全模式。

如果这不起作用,则最后一种选择是完全关闭 full 模式,并使用 ​fullTemplateTypeCheck: false​,因为在这种情况下,我们已经做了一些特殊的努力来使 Angular 9 向后兼容。

你无法使用任何推荐方式解决的类型检查错误可能是因为模板类型检查器本身存在错误。如果遇到需要退回到基本模式的错误,则很可能是这样的错误。如果发生这种情况,请提出问题,以便开发组解决。

输入属性与类型检查

模板类型检查器会检查绑定表达式的类型是否与相应指令输入的类型兼容。比如,请考虑以下组件:

export interface User {
name: string;
}
@Component({
selector: 'user-detail',
template: '{{ user.name }}',
})
export class UserDetailComponent {
@Input() user: User;
}

AppComponent ​模板按以下方式使用此组件:

@Component({
selector: 'app-root',
template: '<user-detail [user]="selectedUser"></user-detail>',
})
export class AppComponent {
selectedUser: User | null = null;
}

这里,在检查 ​AppComponent ​的模板期间,​[user]="selectedUser"​ 绑定与 ​UserDetailComponent.user​ 输入属性相对应。因此,Angular 会将 ​selectedUser ​属性赋值给 ​UserDetailComponent.user​,如果它们的类型不兼容,则将导致错误。TypeScript 会根据其类型系统进行赋值检查,并遵循在应用程序中配置的标志(比如 ​strictNullChecks​)。

通过向模板类型检查器提出更具体的模板内类型要求,可以避免一些运行时类型错误。通过在指令定义中提供各种“模板守卫”功能,可以让自定义指令的输入类型要求尽可能具体。

严格的空检查

当你启用 ​strictTemplates ​和 TypeScript 标志 ​strictNullChecks​,在某些情况下可能会发生类型检查错误,这些情况很难避免。比如:

  • 一个可空值,该值绑定到未启用 ​strictNullChecks ​的库中的指令。

对于没有使用 ​strictNullChecks ​编译的库,其声明文件将不会指示字段是否可以为 ​null​。对于库正确处理 ​null ​的情况,这是有问题的,因为编译器将根据声明文件进行空值检查,而它省略了 ​null ​类型。这样,编译器会产生类型检查错误,因为它要遵守 ​strictNullChecks​。

  • 将 ​async ​管道与 Observable 一起使用会同步发出值。

async ​管道当前假定它预订的 Observable 可以是异步的,这意味着可能还没有可用的值。在这种情况下,它仍然必须返回某些内容 —— ​null​。换句话说,​async ​管道的返回类型包括 ​null​,这在知道此 Observable 会同步发出非空值的情况下可能会导致错误。

对于上述问题,有两种潜在的解决方法:

  • 在模板中,包括非空断言运算符 ​!​ 用在可为空的表达式的末尾,比如
<user-detail [user]="user!"></user-detail>

在此范例中,编译器在可空性方面会忽略类型不兼容,就像在 TypeScript 代码中一样。对于 ​async ​管道,请注意,表达式需要用括号括起来,如

<user-detail [user]="(user$ | async)!"></user-detail>
完全禁用 Angular 模板中的严格空检查。

当启用 ​strictTemplates ​时,仍然可以禁用类型检查的某些方面。将选项 ​strictNullInputTypes ​设置为 ​false ​将禁用 Angular 模板中的严格空检查。此标志会作用于应用程序中包含的所有组件。

给库作者的建议

作为库作者,你可以采取多种措施为用户提供最佳体验。首先,启用 ​strictNullChecks ​并在输入的类型中包括 ​null​(如果适用),可以与消费者沟通,看他们是否可以提供可空的值。

输入 setter 强制类型转换

有时,指令或组件的 ​@Input() ​最好更改绑定到它的值,通常使用此输入的 getter / setter 对。比如,考虑以下自定义按钮组件:

考虑以下指令:

@Component({
selector: 'submit-button',
template: `
<div class="wrapper">
<button [disabled]="disabled">Submit</button>
</div>
`,
})
class SubmitButton {
private _disabled: boolean;
@Input()
get disabled(): boolean {
return this._disabled;
}
set disabled(value: boolean) {
this._disabled = value;
}
}

在这里,组件的输入 ​disabled ​将传给模板中的 ​<button>​。只要将 ​boolean ​值绑定到输入,所有这些工作都可以按预期进行。但是,假设使用者使用模板中的这个输入作为属性:

<submit-button disabled></submit-button>

这与绑定具有相同的效果:

<submit-button [disabled]="''"></submit-button>

在运行时,输入将设置为空字符串,这不是 ​boolean ​值。处理此问题的角组件库通常将值“强制转换”到 setter 中的正确类型中:

set disabled(value: boolean) {
this._disabled = (value === '') || value;
}

最好在这里将 ​value ​的类型从 ​boolean ​更改为 ​boolean|''​ 以匹配 setter 实际会接受的一组值。TypeScript 4.3 之前的版本要求 getter 和 setter 的类型相同,因此,如果 getter 要返回 ​boolean ​则 setter 会卡在较窄的类型上。

如果消费者对模板启用了 Angular 的最严格的类型检查功能,则会产生一个问题:空字符串 ​''​ 实际上无法赋值给 ​disabled ​字段,使用属性格式写会产生类型错误。

作为解决此问题的一种取巧方式,Angular 支持对 ​@Input()​ 检查比声明的输入字段更宽松的类型。通过向组件类添加带有 ​ngAcceptInputType_ ​前缀的静态属性来启用此功能:

class SubmitButton {
private _disabled: boolean;
@Input()
get disabled(): boolean {
return this._disabled;
}
set disabled(value: boolean) {
this._disabled = (value === '') || value;
}
static ngAcceptInputType_disabled: boolean|'';
}

从 TypeScript 4.3 开始,setter 能够声明为接受 ​boolean|''​ 类型,这就让输入属性 setter 强制类型转换字段过时了。因此,输入属性 setter 强制类型转换字段也就弃用了。

该字段不需要值。它只要存在就会通知 Angular 的类型检查器,​disabled ​输入应被视为接受与 ​boolean|''​ 类型匹配的绑定。后缀应为 ​@Input​ 字段的名称。

请注意,如果给定输入存在 ​ngAcceptInputType_​ 覆盖,则设置器应能够处理任何覆盖类型的值。

使用 $any() 禁用类型检查

可以通过把绑定表达式包含在类型转换伪函数 ​$any()​ 中来禁用类型检查。编译器会像在 TypeScript 中使用 ​<any>​ 或 ​as any​ 进行类型转换一样对待它。

在以下范例中,将 ​person ​强制转换为 ​any ​类型可以压制错误 ​Property address does not exist​。

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