Angular 模板類型檢查

2022-07-15 10:09 更新

模板類型檢查概述

正如 TypeScript 在代碼中捕獲類型錯誤一樣,Angular 也會檢查應用程序模板中的表達式和綁定,并可以報告所發(fā)現(xiàn)的任何類型錯誤。Angular 當前有三種執(zhí)行此操作的模式,具體取決于 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 ?類型結束,這可能導致表達式的后續(xù)部分不受檢查。

完全模式

如果將 ?fullTemplateTypeCheck ?標志設置為 ?true?,則 Angular 在模板中進行類型檢查時會更加主動。特別是:

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

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

  • 對 DOM 元素的本地引用。
  • ?$event? 對象
  • 安全導航表達式

?fullTemplateTypeCheck ?標志已經在 Angular 13 中棄用了。它被編譯器選項中的 ?strictTemplates ?家族代替了。

嚴格模式

Angular 延續(xù)了 ?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?。

排除模板錯誤

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

當 Angular 庫的類型不完整或不正確,或者在以下情況下類型與預期不完全一致時,也可能存在誤報。

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

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

  • 在某些情況下,使用 ?$any()類型轉換函數(shù)可以選擇不對部分表達式進行類型檢查
  • 你可以通過在應用程序的 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

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

strictLiteralTypes

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

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

如果這不起作用,則最后一種選擇是完全關閉 full 模式,并使用 ?fullTemplateTypeCheck: false?,因為在這種情況下,我們已經做了一些特殊的努力來使 Angular 9 向后兼容。

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

輸入屬性與類型檢查

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

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 會根據其類型系統(tǒng)進行賦值檢查,并遵循在應用程序中配置的標志(比如 ?strictNullChecks?)。

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

嚴格的空檢查

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

  • 一個可空值,該值綁定到未啟用 ?strictNullChecks ?的庫中的指令。
  • 對于沒有使用 ?strictNullChecks ?編譯的庫,其聲明文件將不會指示字段是否可以為 ?null?。對于庫正確處理 ?null ?的情況,這是有問題的,因為編譯器將根據聲明文件進行空值檢查,而它省略了 ?null ?類型。這樣,編譯器會產生類型檢查錯誤,因為它要遵守 ?strictNullChecks?。

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

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

  • 在模板中,包括非空斷言運算符 ?!? 用在可為空的表達式的末尾,比如
  • <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_ ?前綴的靜態(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 強制類型轉換字段過時了。因此,輸入屬性 setter 強制類型轉換字段也就棄用了。

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

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

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

可以通過把綁定表達式包含在類型轉換偽函數(shù) ?$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;
}


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號