Angular 模板类型检查
模板类型检查概述
正如 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
|
是否检查使用文本属性进行的 (将 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>
当启用 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;
}