範本型別檢查總覽
就像 TypeScript 會抓出你程式碼中的型別錯誤一樣,Angular 會檢查你應用程式範本中的運算式與繫結,並能回報它找到的任何型別錯誤。
預設情況下,Angular 以嚴格模式檢查範本。
如有需要,你可以把 Angular 編譯器選項 中的 strictTemplates 旗標設為 false,暫時退出嚴格模式,改回較不徹底的基本模式。
基本模式
在基本型別檢查模式中(strictTemplates 旗標設為 false),Angular 只驗證範本中的最上層運算式。
如果你寫了 <map [city]="user.address.city">,編譯器會驗證下列事項:
user是元件類別上的一個屬性user是一個帶有 address 屬性的物件user.address是一個帶有 city 屬性的物件
編譯器不會驗證 user.address.city 的值是否可指派給 <map> 元件的 city 輸入。
編譯器在這個模式下也有一些主要限制:
- 重要的是,它不會檢查內嵌檢視,例如
@if、@for、*ngIf、*ngFor或其他<ng-template>內嵌檢視。 - 它不會推斷
#refs的型別、管道的結果,或事件繫結中$event的型別。
在許多情況下,這些東西最終會是 any 型別,導致運算式後續的部分未經檢查。
嚴格模式
嚴格模式是預設模式,在 strictTemplates 旗標為 true 或未設定時啟用。
在嚴格模式下,Angular 會執行下列事項:
- 檢查內嵌檢視(例如
@if、@for、*ngIf或*ngFor內的檢視) - 推斷管道的正確傳回型別
- 推斷指向指令與管道之本機參考的正確型別
- 驗證元件/指令的繫結可指派給它們的
input() - 在驗證輸入繫結時遵循 TypeScript 的
strictNullChecks旗標 - 推斷元件/指令的正確型別,包括泛型
- 在有設定的地方推斷範本脈絡型別(舉例來說,讓
NgFor能被正確地型別檢查) - 推斷元件/指令、DOM 與動畫事件繫結中
$event的正確型別 - 根據標籤名稱,推斷指向 DOM 元素之本機參考的正確型別(舉例來說,就是
document.createElement對該標籤會傳回的型別)
*ngFor 的檢查
兩種型別檢查模式對內嵌檢視的處理方式不同。 請看下列範例。
User interface
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 知道 <span> 中的 user 型別是 User,且 address 是一個帶有 string 型別 city 屬性的物件。
範本錯誤疑難排解
使用嚴格模式時,你可能會遇到基本模式下不會出現的範本錯誤。 這些錯誤往往代表範本中真正的型別不符,先前的工具並沒有抓出來。 如果是這種情況,錯誤訊息應該能清楚指出範本中問題發生的位置。
當 Angular 程式庫的型別定義不完整或不正確,或是型別定義與預期不完全一致時,也可能出現誤報,如下列情況。
當程式庫的型別定義錯誤或不完整時(舉例來說,如果程式庫撰寫時沒有考慮
strictNullChecks,就可能缺少null | undefined)當程式庫的輸入型別太窄,且程式庫沒有加入適當的中繼資料讓 Angular 推斷出來時。 這通常發生在 disabled 或其他常見的、被用作屬性的布林輸入上,舉例來說,
<input disabled>。在 DOM 事件中使用
$event.target時(因為事件冒泡的可能性,DOM 型別定義中的$event.target並沒有你可能預期的型別)
遇到這類誤報時,有幾個選擇:
- 在特定脈絡下使用
$any()型別轉換函式,讓運算式的一部分退出型別檢查 - 在應用程式的 TypeScript 設定檔
tsconfig.json中設定strictTemplates: false,完全停用嚴格檢查 - 把某個_嚴格旗標_設為
false,單獨停用特定的型別檢查操作,同時在其他方面保持嚴格 - 如果你想同時使用
strictTemplates與strictNullChecks,可以使用strictNullInputTypes,特別針對輸入繫結退出嚴格的 null 型別檢查
下列每個選項的值都跟隨 strictTemplates(strictTemplates 為 true 時就是 true,反之亦然):
| 嚴格旗標 | 效果 |
|---|---|
strictInputTypes |
是否檢查繫結運算式對 @Input() 欄位的可指派性。也會影響指令泛型型別的推斷。 |
strictNullInputTypes |
檢查 @Input() 繫結時是否遵循 strictNullChecks(依 strictInputTypes)。使用撰寫時沒有考慮 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 |
$event 在繫結到元件/指令的 @Output() 或動畫事件的事件繫結中,是否具有正確的型別。如果停用,它會是 any。 |
strictDomEventTypes |
$event 在繫結到 DOM 事件的事件繫結中,是否具有正確的型別。如果停用,它會是 any。 |
strictContextGenerics |
泛型元件的型別參數是否會被正確推斷(包括任何泛型邊界)。如果停用,任何型別參數都會是 any。 |
strictLiteralTypes |
在範本中宣告的物件與陣列字面值,其型別是否會被推斷。如果停用,這類字面值的型別會是 any。 |
其他選項(與 strictTemplates 無關):
| 嚴格旗標 | 效果 |
|---|---|
strictInputAccessModifiers |
把繫結運算式指派給 @Input() 或 input() 時,是否遵循 private/protected/readonly 等存取修飾詞。如果停用,輸入的存取修飾詞會被忽略,只檢查型別。注意:這項檢查只適用於輸入,不適用於輸出。 |
strictUnclaimedEventNames |
是否回報名稱既不符合套用到該元素之指令的任何輸出、也不符合已知原生 DOM 事件的事件繫結——原生 DOM 事件指 TypeScript 的 GlobalEventHandlersEventMap 中描述的那些,比對時不分大小寫——這通常表示輸出名稱拼錯了。只有單一識別字、採小駝峰寫法的名稱會被檢查;以連字號分隔的名稱(舉例來說,my-event)不在此限,因為後代元素派發的自訂事件慣例上使用這種名稱;使用 CUSTOM_ELEMENTS_SCHEMA 時,沒有比對到元件的元素也同樣不在此限。這個選項預設為 false,即使 strictTemplates 設為 true 也是如此。 |
如果用這些旗標疑難排解後仍然有問題,最後的手段是停用 strictTemplates,退回基本模式。
任何建議方法都無法解決的型別檢查錯誤,可能是範本型別檢查器本身的錯誤所致。 如果你遇到的錯誤需要退回基本模式,那很可能就是這類錯誤。 如果發生這種情況,請提出 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 屬性指派給 UserDetailComponent.user,如果兩者的型別不相容,就會產生錯誤。
TypeScript 依其型別系統檢查這次指派,並遵循應用程式中設定的 strictNullChecks 等旗標。
為範本型別檢查器提供更具體的範本內型別要求,以避免執行階段的型別錯誤。 在指令定義中提供範本守衛函式,讓你自己的指令的輸入型別要求盡可能具體。 請參閱本指南的改善自訂指令的範本型別檢查。
嚴格的 null 檢查
當你啟用 strictTemplates 與 TypeScript 旗標 strictNullChecks 時,某些不容易避開的情況可能會出現型別檢查錯誤。
For example:
一個可為 null 的值,被繫結到來自某個未啟用
strictNullChecks的程式庫的指令。對於未以
strictNullChecks編譯的程式庫,它的宣告檔案不會指出欄位是否可以是null。 對於程式庫能正確處理null的情況,這會造成問題,因為編譯器會拿可為 null 的值去對照省略了null型別的宣告檔案。 因此,編譯器因為遵循strictNullChecks而產生型別檢查錯誤。把
async管道用在你明知會同步發出值的 Observable 上。async管道目前假設它訂閱的 Observable 可能是非同步的,也就是說,可能還沒有可用的值。 在這種情況下,它仍然必須傳回某個東西,也就是null。 換句話說,async管道的傳回型別包含null,在已知 Observable 會同步發出不可為 null 的值的情況下,這可能會導致錯誤。
針對上述問題,有兩個可能的因應方法:
- 在範本中,在可為 null 的運算式結尾加上非 null 斷言運算子
!,例如
<user-detail [user]="user!"></user-detail>
在這個範例中,編譯器會忽略可空性方面的型別不相容,就像在 TypeScript 程式碼中一樣。
如果是 async 管道的情況,請注意運算式需要用括號包起來,例如
<user-detail [user]="(user$ | async)!"></user-detail>
完全停用 Angular 範本中的嚴格 null 檢查。
啟用
strictTemplates時,仍然可以停用型別檢查的某些方面。 把選項strictNullInputTypes設為false,就能停用 Angular 範本中的嚴格 null 檢查。 這個旗標適用於應用程式中的所有元件。
給程式庫作者的建議
身為程式庫作者,你可以採取幾項措施,為使用者提供最佳體驗。
首先,啟用 strictNullChecks,並在適當時把 null 包含在輸入的型別中,能告訴你的使用者他們是否可以提供可為 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 值繫結到該輸入,這一切都會如預期般運作。
但假設使用者在範本中把這個輸入用作屬性:
這與下列繫結有相同的效果:
在執行階段,該輸入會被設為空字串,而空字串不是 boolean 值。
處理這個問題的 Angular 元件程式庫,通常會在 setter 中把值「強制轉型」為正確的型別:
set disabled(value: boolean) {
this._disabled = (value === '') || value;
}
理想情況下,應該把這裡 value 的型別從 boolean 改為 boolean|'',以符合 setter 實際接受的值的集合。
4.3 版之前的 TypeScript 要求 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_ 覆寫,那麼 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;
}