範本型別檢查

edit

範本型別檢查概覽

正如 TypeScript 可以捕獲你程式碼中的型別錯誤一樣,Angular 也會檢查你的應用範本中的表示式和繫結,並報告它發現的任何型別錯誤。 Angular 目前有三種執行此操作的模式,具體取決於 Angular 編譯器選項fullTemplateTypeCheckstrictTemplates 標誌的值。

基本模式

在最基本的型別檢查模式下,當 fullTemplateTypeCheck 標誌設定為 false 時,Angular 僅驗證範本中的頂層表示式。

如果你編寫了 <map [city]="user.address.city">,編譯器會驗證以下內容:

編譯器不驗證 user.address.city 的值是否可賦值給 <map> 元件的 city 輸入。

在這種模式下,編譯器還有一些主要的侷限性:

在許多情況下,這些東西最終都變成 any 型別,這可能會導致表示式的後續部分未經檢查。

完整模式

如果 fullTemplateTypeCheck 標誌設定為 true,Angular 會在範本中更積極地進行型別檢查。 特別是:

以下各項仍然是 any 型別。

重要提示: fullTemplateTypeCheck 標誌已在 Angular 13 中被棄用。 應該改用 strictTemplates 系列的編譯器選項。

嚴格模式

Angular 保留了 fullTemplateTypeCheck 標誌的行為,並引入了第三種『嚴格模式』。 嚴格模式是完整模式的超集,可以透過將 strictTemplates 標誌設定為 true 來訪問。 此標誌取代了 fullTemplateTypeCheck 標誌。

除了完整模式的行為之外,Angular 還執行以下操作:

檢查 *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 會檢查 configuser 是否存在,並假定型別為 any。 在嚴格模式下,Angular 知道 <span> 中的 user 具有 User 型別,並且 address 是一個物件,其 city 屬性的型別為 string

排查範本錯誤

在嚴格模式下,你可能會遇到在以前的任何模式下都不會出現的範本錯誤。 這些錯誤通常表示範本中真正的型別不對應,而以前的工具沒有捕獲到這些不對應。 如果是這種情況,則錯誤訊息應明確指出範本中出現問題的具體位置。

當 Angular 函式庫的型別定義不完整或不正確,或者當型別定義與預期不完全一致時,也可能出現誤報,如下例所示。

如果遇到像這樣的誤報,有以下幾種選擇:

除非另有註解,否則以下每個選項都設定為 strictTemplates 的值(當 strictTemplatestrue 時為 true,反之亦然)。

嚴格性標誌 效果
strictInputTypes 是否檢查繫結表示式對 @Input() 欄位的可賦值性。還會影響指令泛型型別的推斷。
strictInputAccessModifiers 在將繫結表示式賦值給 @Input() 時,是否考慮 private/protected/readonly 等訪問修飾符。如果停用,則會忽略 @Input 的訪問修飾符;只檢查型別。即使將 strictTemplates 設定為 true,此選項的預設值也為 false
strictNullInputTypes 在檢查 @Input() 繫結時(根據 strictInputTypes),是否考慮 strictNullChecks。停用此選項在使用未考慮 strictNullChecks 建立的函式庫時可能很有用。
strictAttributeTypes 是否檢查使用文字屬性進行的 @Input() 繫結。例如,<input matInput disabled="true">(將 disabled 屬性設定為字串 'true')與 <input matInput [disabled]="true">(將 disabled 屬性設定為布林值 true)的比較。
strictSafeNavigationTypes 是否正確推斷安全導向操作的回傳型別(例如,user?.name 將根據 user 的型別正確推斷)。如果停用,user?.name 的型別將為 any
strictDomLocalRefTypes DOM 元素的本地引用是否具有正確的型別。如果停用,對於 <input #ref>ref 的型別將為 any
strictOutputEventTypes 對於元件/指令 @Output() 的事件繫結或動畫事件的繫結,$event 是否具有正確的型別。如果停用,它將為 any
strictDomEventTypes 對於 DOM 事件的事件繫結,$event 是否具有正確的型別。如果停用,它將是 any
strictContextGenerics 是否正確推斷泛型元件的型別引數(包括任何泛型邊界)。如果停用,任何型別引數都將是 any
strictLiteralTypes 是否推斷範本中宣告的物件和數組字面量的型別。如果停用,此類別字面量的型別將為 any。當設定了 fullTemplateTypeCheckstrictTemplates 之一時,此標誌為 true

如果在透過這些標誌進行排查後仍然遇到問題,請透過停用 strictTemplates 回退到完整模式。

如果這不起作用,最後的選擇是使用 fullTemplateTypeCheck: false 完全關閉完整模式。

你無法使用任何推薦方法求解的型別檢查錯誤可能是範本型別檢查器本身的錯誤造成的。 如果你遇到的錯誤需要回退到基本模式,則很可能是這種錯誤。 如果發生這種情況,請送出 issue,以便團隊可以解決它。

輸入和型別檢查

範本型別檢查器會檢查繫結表示式的型別是否與相應指令輸入的型別相容。 例如,考慮以下元件:

export interface User {  name: string;}@Component({  selector: 'user-detail',  template: '{{ user.name }}',})export class UserDetailComponent {  user = input.required<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 屬性(Property)賦值給 UserDetailComponent.user,如果它們的型別不相容,這將導致錯誤。 TypeScript 根據其型別體系檢查賦值,並遵循諸如 strictNullChecks 之類別的標誌,因為它們是在應用中設定的。

透過為範本型別檢查器提供更具體的範本內型別要求,避免執行時型別錯誤。 透過在指令定義中提供範本保護函式,使你自己的指令的輸入型別要求儘可能具體。 請參閱本指南中的改進自定義指令的範本型別檢查

嚴格的 Null 檢查

當你啟用 strictTemplates 和 TypeScript 標誌 strictNullChecks 時,在某些情況下可能會發生型別檢查錯誤,而這些情況可能不容易避免。 例如:

對於上述問題,有兩種可能的解決方法:

給函式庫作者的建議

作為函式庫作者,你可以採取一些措施來為你的使用者提供最佳體驗。 首先,啟用 strictNullChecks 並在輸入的型別中包含 null(如果適用),可以告知你的使用者他們是否可以提供可空值。 此外,還可以提供特定於範本型別檢查器的型別提示。 請參閱改進自定義指令的範本型別檢查輸入 setter 強制轉換

輸入 setter 強制轉換

有時,指令或元件的 input() 屬性希望更改繫結到它的值,通常是使用輸入的 transform 函式。 例如,請考慮這個自定義按鈕元件:

考慮以下指令:

@Component({  selector: 'submit-button',  template: `    <div class="wrapper">      <button [disabled]="disabled">Submit</button>    </div>  `,})class SubmitButton {  disabled = input.required({transform: booleanAttribute });}

在這裡,元件的 disabled 輸入正在傳遞到範本中的 <button>。 只要將 boolean 值繫結到輸入,所有這些都按預期工作。 但是,假設使用者在範本中將此輸入用作 Attribute:

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

這與以下繫結具有相同的效果:

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

在執行時,輸入將設定為空字串,這不是 boolean 值。 處理此問題的 Angular 元件庫通常會在 setter 中將值『強制轉換』為正確的型別:

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

理想的情況是將此處的 value 型別從 boolean 更改為 boolean|'',以對應 setter 實際接受的值集。 TypeScript 4.3 之前的版本要求 getter 和 setter 具有相同的型別,因此如果 getter 應該回傳 boolean,則 setter 會停留在較窄的型別上。

如果使用者為範本啟用了 Angular 最嚴格的型別檢查,則會產生一個問題:空字串 ('') 實際上不可賦值給 disabled 欄位,這在使用 Attribute 形式時會產生型別錯誤。

作為此問題的解決方法,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_ 重寫,則 setter 應該能夠處理任何重寫型別的值。

使用 $any() 停用型別檢查

透過將繫結表示式包圍在對 $any() 強制轉換偽函式的呼叫中,停用對繫結表示式的檢查。 編譯器將其視為強制轉換為 any 型別,就像在 TypeScript 中使用 <any>as any 強制轉換一樣。

在以下範例中,將 person 強制轉換為 any 型別會阻止錯誤 Property address does not exist

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