Angular 模板類型檢查

2022-07-15 10:09 更新

模板類型檢查概述

正如 TypeScript 在代碼中捕獲類型錯(cuò)誤一樣,Angular 也會檢查應(yīng)用程序模板中的表達(dá)式和綁定,并可以報(bào)告所發(fā)現(xiàn)的任何類型錯(cuò)誤。Angular 當(dāng)前有三種執(zhí)行此操作的模式,具體取決于 TypeScript 配置文件 中的 ?fullTemplateTypeCheck ?和 ?strictTemplates ?標(biāo)志的值。

基本模式

在最基本的類型檢查模式下,將 ?fullTemplateTypeCheck ?標(biāo)志設(shè)置為 ?false?,Angular 僅驗(yàn)證模板中的頂層表達(dá)式。

如果編寫 ?<map [city]="user.address.city">?,則編譯器將驗(yàn)證以下內(nèi)容:

  • ?user ?是該組件類的屬性
  • ?user ?是具有 ?address ?屬性的對象
  • ?user.address? 是具有 ?city ?屬性的對象

編譯器不會驗(yàn)證 ?user.address.city? 的值是否可賦值給 ?<map>? 組件的輸入屬性 ?city?。

編譯器在此模式下也有一些主要限制:

  • 重要的是,它不會檢查嵌入式視圖,比如 ?*ngIf?,?*ngFor? 和其它 ?<ng-template>? 嵌入式視圖。
  • 它無法弄清 ?#refs? 的類型、管道的結(jié)果、事件綁定中 ?$event? 的類型等等。

在許多情況下,這些東西最終都以 ?any ?類型結(jié)束,這可能導(dǎo)致表達(dá)式的后續(xù)部分不受檢查。

完全模式

如果將 ?fullTemplateTypeCheck ?標(biāo)志設(shè)置為 ?true?,則 Angular 在模板中進(jìn)行類型檢查時(shí)會更加主動(dòng)。特別是:

  • 檢查嵌入式視圖(比如 ?*ngIf? 或 ?*ngFor? 內(nèi)的 ?*ngFor?)
  • 管道具有正確的返回類型
  • 對指令和管道的本地引用具有正確的類型(any 泛型參數(shù)除外,該通用參數(shù)將是 ?any?)

以下仍然具有 ?any ?類型。

  • 對 DOM 元素的本地引用。
  • ?$event? 對象
  • 安全導(dǎo)航表達(dá)式

?fullTemplateTypeCheck ?標(biāo)志已經(jīng)在 Angular 13 中棄用了。它被編譯器選項(xiàng)中的 ?strictTemplates ?家族代替了。

嚴(yán)格模式

Angular 延續(xù)了 ?fullTemplateTypeCheck ?標(biāo)志的行為,并引入了第三個(gè)“嚴(yán)格模式”。嚴(yán)格模式是完全模式的超集,可以通過將 ?strictTemplates ?標(biāo)志設(shè)置為 true 來訪問。該標(biāo)志取代 ?fullTemplateTypeCheck ?標(biāo)志。在嚴(yán)格模式下,Angular 添加了超出 8 版類型檢查器的檢查。

注意:
嚴(yán)格模式僅在使用 Ivy 時(shí)可用。

除了完全模式的行為之外,Angular 版本 9 還會:

  • 驗(yàn)證組件/指令綁定是否可賦值給它們的 ?@Input() ?
  • 驗(yàn)證以上模式時(shí),會遵守 TypeScript 的 ?strictNullChecks ?標(biāo)志
  • 推斷組件/指令的正確類型,包括泛型
  • 推斷配置模板上下文的類型(比如,允許對 ?NgFor ?進(jìn)行正確的類型檢查)
  • 在組件/指令、DOM 和動(dòng)畫事件綁定中推斷 ?$event? 的正確類型
  • 根據(jù)標(biāo)簽(tag)名稱(比如,?document.createElement? 將為該標(biāo)簽返回正確的類型),推斷出對 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 不會檢查它們中的任何一個(gè)。但是,在完全模式下,Angular 會檢查 ?config ?和 ?user ?是否存在,并假設(shè)為 ?any ?的類型。在嚴(yán)格模式下,Angular 知道該 ?user ?在 ?<span>? 中是 ?User ?類型,而 ?address ?是與一個(gè)對象,它有一個(gè) ?string ?類型的屬性 ?city?。

排除模板錯(cuò)誤

使用嚴(yán)格模式,你可能會遇到在以前的兩種模式下都沒有出現(xiàn)過的模板錯(cuò)誤。這些錯(cuò)誤通常表示模板中的真正類型不匹配,而以前的工具并未捕獲這些錯(cuò)誤。在這種情況下,該錯(cuò)誤消息會使該問題在模板中的位置清晰可見。

當(dāng) Angular 庫的類型不完整或不正確,或者在以下情況下類型與預(yù)期不完全一致時(shí),也可能存在誤報(bào)。

  • 當(dāng)庫的類型錯(cuò)誤或不完整時(shí)(比如,如果編寫庫的時(shí)候沒有注意 ?strictNullChecks?,則可能缺少 ?null | undefined?)
  • 當(dāng)庫的輸入類型太窄并且?guī)鞗]有為 Angular 添加適當(dāng)?shù)脑獢?shù)據(jù)來解決這個(gè)問題時(shí)。這通常在禁用或使用其它通用布爾輸入作為屬性時(shí)發(fā)生,比如 ?<input disabled>?。
  • 在將 ?$event.target? 用于 DOM 事件時(shí)(由于事件冒泡的可能性,DOM 類型中的 ?$event.target? 不具有你可能期望的類型)

如果發(fā)生此類誤報(bào),則有以下幾種選擇:

  • 在某些情況下,使用 ?$any()類型轉(zhuǎn)換函數(shù)可以選擇不對部分表達(dá)式進(jìn)行類型檢查
  • 你可以通過在應(yīng)用程序的 TypeScript 配置文件 ?tsconfig.json? 中設(shè)置 ?strictTemplates: false? 來完全禁用嚴(yán)格檢查
  • 通過將嚴(yán)格性標(biāo)志設(shè)置為 ?false?,可以在保持其它方面的嚴(yán)格性的同時(shí),單獨(dú)禁用某些特定的類型檢查操作
  • 如果要一起使用 ?strictTemplates ?和 ?strictNullChecks?,則可以通過 ?strictNullInputTypes ?來選擇性排除專門用于輸入綁定的嚴(yán)格空類型檢查

除非另行說明,下面的每個(gè)選項(xiàng)都會設(shè)置為 ?strictTemplates ?的值(當(dāng) ?strictTemplates ?為真時(shí)是 ?true?,其他值也一樣)。

嚴(yán)格標(biāo)志

影響

strictInputTypes

是否檢查綁定表達(dá)式對 `@Input()` 字段的可賦值性。也會影響指令泛型類型的推斷。

strictInputAccessModifiers

在把綁定表達(dá)式賦值給 `@Input()` 時(shí),是否檢查像 `private`/`protected`/`readonly` 這樣的訪問修飾符。如果禁用,則 `@Input` 上的訪問修飾符會被忽略,只進(jìn)行類型檢查。本選項(xiàng)默認(rèn)為 `false`,即使當(dāng) `strictTemplates` 為 `true` 時(shí)也一樣。

strictNullInputTypes

檢查 `@Input()` 綁定時(shí)是否要 `strictNullChecks`(對于每個(gè) `strictInputTypes`)。當(dāng)使用的庫不是基于 `strictNullChecks` 構(gòu)建的時(shí),將其關(guān)閉會很有幫助。

strictAttributeTypes

是否檢查使用文本屬性進(jìn)行的 @Input() 綁定。例如,

<input matInput disabled="true">
(將 disabled 屬性設(shè)置為字符串 'true'
<input matInput [disabled]="true">
(將 disabled 屬性設(shè)置為布爾值 true)。
strictSafeNavigationTypes

是否根據(jù) `user` 的類型正確推斷出安全導(dǎo)航操作的返回類型(比如 `user?.name`)。如果禁用,則 `user?.name` 的類型為 `any`。

strictDomLocalRefTypes

對 DOM 元素的本地引用是否將具有正確的類型。如果禁用,對于 `` 來說 `ref` 會是 `any` 類型的。

strictOutputEventTypes

對于綁定到組件/指令 `@Output()` 或動(dòng)畫事件的事件綁定,`$event` 是否具有正確的類型。如果禁用,它將為 `any`。

strictDomEventTypes

對于與 DOM 事件的事件綁定,`$event` 是否具有正確的類型。如果禁用,它將為 `any`。

strictContextGenerics

泛型組件的類型參數(shù)是否應(yīng)該被正確推斷(包括泛型上界和下界). 如果禁用它,所有的類型參數(shù)都會被當(dāng)做 `any`。

strictLiteralTypes

是否要推斷模板中聲明的對象和數(shù)組字面量的類型。如果禁用,則此類文字的類型就是 `any`。當(dāng) `fullTemplateTypeCheck` 或 `strictTemplates` 為 `true` 時(shí),此標(biāo)志為 `true`。

如果使用這些標(biāo)志進(jìn)行故障排除后仍然存在問題,可以通過禁用 ?strictTemplates ?退回到完全模式。

如果這不起作用,則最后一種選擇是完全關(guān)閉 full 模式,并使用 ?fullTemplateTypeCheck: false?,因?yàn)樵谶@種情況下,我們已經(jīng)做了一些特殊的努力來使 Angular 9 向后兼容。

你無法使用任何推薦方式解決的類型檢查錯(cuò)誤可能是因?yàn)槟0孱愋蜋z查器本身存在錯(cuò)誤。如果遇到需要退回到基本模式的錯(cuò)誤,則很可能是這樣的錯(cuò)誤。如果發(fā)生這種情況,請提出問題,以便開發(fā)組解決。

輸入屬性與類型檢查

模板類型檢查器會檢查綁定表達(dá)式的類型是否與相應(yīng)指令輸入的類型兼容。比如,請考慮以下組件:

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? 輸入屬性相對應(yīng)。因此,Angular 會將 ?selectedUser ?屬性賦值給 ?UserDetailComponent.user?,如果它們的類型不兼容,則將導(dǎo)致錯(cuò)誤。TypeScript 會根據(jù)其類型系統(tǒng)進(jìn)行賦值檢查,并遵循在應(yīng)用程序中配置的標(biāo)志(比如 ?strictNullChecks?)。

通過向模板類型檢查器提出更具體的模板內(nèi)類型要求,可以避免一些運(yùn)行時(shí)類型錯(cuò)誤。通過在指令定義中提供各種“模板守衛(wèi)”功能,可以讓自定義指令的輸入類型要求盡可能具體。

嚴(yán)格的空檢查

當(dāng)你啟用 ?strictTemplates ?和 TypeScript 標(biāo)志 ?strictNullChecks?,在某些情況下可能會發(fā)生類型檢查錯(cuò)誤,這些情況很難避免。比如:

  • 一個(gè)可空值,該值綁定到未啟用 ?strictNullChecks ?的庫中的指令。
  • 對于沒有使用 ?strictNullChecks ?編譯的庫,其聲明文件將不會指示字段是否可以為 ?null?。對于庫正確處理 ?null ?的情況,這是有問題的,因?yàn)榫幾g器將根據(jù)聲明文件進(jìn)行空值檢查,而它省略了 ?null ?類型。這樣,編譯器會產(chǎn)生類型檢查錯(cuò)誤,因?yàn)樗袷?nbsp;?strictNullChecks?。

  • 將 ?async ?管道與 Observable 一起使用會同步發(fā)出值。
  • ?async ?管道當(dāng)前假定它預(yù)訂的 Observable 可以是異步的,這意味著可能還沒有可用的值。在這種情況下,它仍然必須返回某些內(nèi)容 —— ?null?。換句話說,?async ?管道的返回類型包括 ?null?,這在知道此 Observable 會同步發(fā)出非空值的情況下可能會導(dǎo)致錯(cuò)誤。

對于上述問題,有兩種潛在的解決方法:

  • 在模板中,包括非空斷言運(yùn)算符 ?!? 用在可為空的表達(dá)式的末尾,比如
  • <user-detail [user]="user!"></user-detail>

    在此范例中,編譯器在可空性方面會忽略類型不兼容,就像在 TypeScript 代碼中一樣。對于 ?async ?管道,請注意,表達(dá)式需要用括號括起來,如

    <user-detail [user]="(user$ | async)!"></user-detail>
  • 完全禁用 Angular 模板中的嚴(yán)格空檢查。
  • 當(dāng)啟用 ?strictTemplates ?時(shí),仍然可以禁用類型檢查的某些方面。將選項(xiàng) ?strictNullInputTypes ?設(shè)置為 ?false ?將禁用 Angular 模板中的嚴(yán)格空檢查。此標(biāo)志會作用于應(yīng)用程序中包含的所有組件。

給庫作者的建議

作為庫作者,你可以采取多種措施為用戶提供最佳體驗(yàn)。首先,啟用 ?strictNullChecks ?并在輸入的類型中包括 ?null?(如果適用),可以與消費(fèi)者溝通,看他們是否可以提供可空的值。

輸入 setter 強(qiáng)制類型轉(zhuǎn)換

有時(shí),指令或組件的 ?@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 ?值綁定到輸入,所有這些工作都可以按預(yù)期進(jìn)行。但是,假設(shè)使用者使用模板中的這個(gè)輸入作為屬性:

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

這與綁定具有相同的效果:

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

在運(yùn)行時(shí),輸入將設(shè)置為空字符串,這不是 ?boolean ?值。處理此問題的角組件庫通常將值“強(qiáng)制轉(zhuǎn)換”到 setter 中的正確類型中:

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

最好在這里將 ?value ?的類型從 ?boolean ?更改為 ?boolean|''? 以匹配 setter 實(shí)際會接受的一組值。TypeScript 4.3 之前的版本要求 getter 和 setter 的類型相同,因此,如果 getter 要返回 ?boolean ?則 setter 會卡在較窄的類型上。

如果消費(fèi)者對模板啟用了 Angular 的最嚴(yán)格的類型檢查功能,則會產(chǎn)生一個(gè)問題:空字符串 ?''? 實(shí)際上無法賦值給 ?disabled ?字段,使用屬性格式寫會產(chǎn)生類型錯(cuò)誤。

作為解決此問題的一種取巧方式,Angular 支持對 ?@Input()? 檢查比聲明的輸入字段更寬松的類型。通過向組件類添加帶有 ?ngAcceptInputType_ ?前綴的靜態(tài)屬性來啟用此功能:

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 強(qiáng)制類型轉(zhuǎn)換字段過時(shí)了。因此,輸入屬性 setter 強(qiáng)制類型轉(zhuǎn)換字段也就棄用了。

該字段不需要值。它只要存在就會通知 Angular 的類型檢查器,?disabled ?輸入應(yīng)被視為接受與 ?boolean|''? 類型匹配的綁定。后綴應(yīng)為 ?@Input字段的名稱。

請注意,如果給定輸入存在 ?ngAcceptInputType_? 覆蓋,則設(shè)置器應(yīng)能夠處理任何覆蓋類型的值。

使用 $any() 禁用類型檢查

可以通過把綁定表達(dá)式包含在類型轉(zhuǎn)換偽函數(shù) ?$any()? 中來禁用類型檢查。編譯器會像在 TypeScript 中使用 ?<any>? 或 ?as any? 進(jìn)行類型轉(zhuǎn)換一樣對待它。

在以下范例中,將 ?person ?強(qiáng)制轉(zhuǎn)換為 ?any ?類型可以壓制錯(cuò)誤 ?Property address does not exist?。

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


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號