Angular CVA 型別安全實作指南
前言
在 Angular 中實作 ControlValueAccessor
(CVA) 時,預設的介面定義使用 any
型別,這會失去 TypeScript 的型別檢查優勢。本文將介紹兩種方法來改善 CVA 的型別安全性。
問題分析
原本的 CVA 實作會遇到型別不明確的問題:
// ❌ 型別不安全的寫法
writeValue(value: any): void {
this.value = value;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
這樣的寫法會失去:
編譯時期的型別檢查
IDE 的自動完成功能
重構時的安全性
方案一:直接型別定義(推薦)
對於單一元件,最簡單的方式是直接為每個方法定義明確的型別。
Checkbox 元件範例
export class CvaCheckboxComponent implements ControlValueAccessor {
private value: boolean = false;
writeValue(value: boolean | null): void {
this.value = value ?? false;
}
registerOnChange(fn: (value: boolean) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
// 私有回調函數也加上型別
private onChange: (value: boolean) => void = () => {};
private onTouched: () => void = () => {};
onCheckboxChange(event: MatCheckboxChange) {
this.value = event.checked;
this.onChange(this.value); // 現在有型別檢查了!
this.onTouched();
}
}
其他常見元件的型別
// Input 元件
writeValue(value: string | null): void
registerOnChange(fn: (value: string | null) => void): void
// Select/Radio 元件
writeValue(value: string | number | null): void
registerOnChange(fn: (value: string | number | null) => void): void
// File Upload 元件
writeValue(value: File | null): void
registerOnChange(fn: (value: File | null) => void): void
優點
實作簡單直接
適合個別元件的客製化需求
不需要額外的基類
缺點
每個 CVA 元件都要重複定義型別
如果有很多 CVA 元件會有重複程式碼
方案二:泛型基類(適合大型專案)
當專案中有多個 CVA 元件時,可以建立一個泛型基類來避免重複程式碼。
檔案結構規劃
泛型基類應該另外創建一個 TypeScript 檔案,建議的檔案結構:
src/
└── shared/
└── components/
└── form/
├── typed-control-value-accessor.ts 👈 泛型基類
├── cva-checkbox/
│ └── cva-checkbox.component.ts
├── cva-input/
│ └── cva-input.component.ts
└── cva-upload-file/
└── cva-upload-file.component.ts
其他可考慮的位置:
src/core/base/typed-control-value-accessor.ts
- 更通用的核心位置src/lib/form/typed-control-value-accessor.ts
- 如果有 shared library
建立泛型基類
typed-control-value-accessor.ts
export abstract class TypedControlValueAccessor<T> implements ControlValueAccessor {
protected value: T | null = null;
protected disabled = false;
writeValue(value: T | null): void {
this.value = value;
}
registerOnChange(fn: (value: T | null) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
// 受保護的回調函數,子類可以使用
protected onChange: (value: T | null) => void = () => {};
protected onTouched: () => void = () => {};
// 子類可以 override 這個方法來處理值的變更
protected updateValue(newValue: T | null): void {
this.value = newValue;
this.onChange(this.value);
this.onTouched();
}
}
使用泛型基類
使用時需要先 import 基類:
// cva-checkbox.component.ts
import { TypedControlValueAccessor } from '../typed-control-value-accessor';
然後繼承基類並指定泛型型別:
// Checkbox 元件
export class CvaCheckboxComponent extends TypedControlValueAccessor<boolean> {
constructor(@Optional() @Self() public ngControl: NgControl) {
super();
if (this.ngControl != null) {
this.ngControl.valueAccessor = this;
}
}
onCheckboxChange(event: MatCheckboxChange) {
this.updateValue(event.checked); // 使用基類的方法
}
}
// Input 元件
export class CvaInputComponent extends TypedControlValueAccessor<string> {
onInputChange(event: Event) {
const target = event.target as HTMLInputElement;
this.updateValue(target.value);
}
}
// File Upload 元件
export class CvaUploadFileComponent extends TypedControlValueAccessor<File> {
onFileSelected(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0] || null;
this.updateValue(file);
}
}
優點
避免重複程式碼
統一的 CVA 實作模式
型別安全且易於維護
可以在基類中加入通用的驗證邏輯
缺點
增加了抽象層級
對於簡單的元件可能過度設計
建議的選擇策略
選擇方案一的情境
專案中只有少數幾個 CVA 元件
每個元件的邏輯差異很大
團隊偏好簡單直接的實作方式
選擇方案二的情境
專案中有多個 CVA 元件(5 個以上)
希望統一 CVA 的實作模式
需要在所有 CVA 元件中加入共通邏輯
實務技巧
1. 處理 null 值
writeValue(value: boolean | null): void {
// 使用 ?? 運算子提供預設值
this.value = value ?? false;
}
2. 型別守衛
writeValue(value: string | null): void {
if (typeof value === 'string') {
this.value = value.trim(); // 安全地使用字串方法
} else {
this.value = '';
}
}
3. 事件處理的型別安全
private onChange: (value: boolean) => void = () => {};
onCheckboxChange(event: MatCheckboxChange) {
this.value = event.checked;
this.onChange(this.value); // TypeScript 會檢查型別匹配
}
結論
兩種方案都能有效提升 CVA 的型別安全性。方案一適合大多數情況,實作簡單且直接;方案二適合有多個 CVA 元件的大型專案,能提供更好的程式碼重用性和一致性。
選擇哪種方案主要取決於專案規模和團隊偏好,但重要的是要擺脫 any
型別,讓 TypeScript 的型別系統發揮最大效用。
Last updated