[TypeScript] 10 分鐘學會 TS 中必會的 5 種型別防禦 Type Guard

2024/03/26閱讀時間約 10 分鐘
Type Guard

Type Guard


會希望寫這篇文章,是有鑑於在學習 TypeScript 後,很常遇到在參數可以輸入多個型別,但卻可能會使用到不同的方法或屬性時,總是會遇到各種困難。為了了解並避免未來遇到這種狀況時,變成一位 any 工程師,因此先寫下這篇文章以供參考。

本文大綱

    • 為什麼會需要型別防禦(Type Guard)?
    • 聯合型別的特性與用途與型別防禦的關聯。
    • 五種常見的型別防禦判斷方式。
    • 結論與延伸。

就讓我們開始來了解,TypeScript 中五種常見的型別防禦方式吧!



為什麼會需要型別防禦(Type Guard)?

在撰寫 TypeScript 的過程中,很常會遇到「我希望參數可以輸入不同的資料型別,但各自的資料卻有不同的方法」。

例如下面的例子:

  interface Car {
drive(): void;
stop(): void;
}

interface Airplane {
fly(): void;
stop(): void;
}

// 下面函式會報錯,之後也會說明如何修正
function getStarted(vehicle: Car | Airplane): Car | Airplane {
if (vehicle.drive) vehicle.drive;
else vehicle.fly;
}

上述的例子是,我們有 CarAirplane 兩個函式,我們各自希望他在 getStart 被呼叫時,可以啟動各自的方法。

那這和 Type Guard 有什麼關係?而之所以需要處理 Type Guard 的原因,就是因為我們的參數 vehicle 有兩種,因此需要特別進行判斷。

開始說明前,也簡單說明 TypeScript 的聯合型別是什麼。




聯合型別的特性與型別防禦(Type Guard)的關聯

聯合型別(Union Type)允許一個變數屬於多種型別之一,提供了靈活處理不同型別值的能力。例如,let myVar: string | number; 聲明了一個變數 myVar,它可以存儲字符串或數字型別的值。這在處理如使用者輸入、API 資料回傳等中特別有用,讓我們能夠在限縮變數的型別時,又保留了一些彈性。

但也因為參數的不確定,導致函式內部的方法也無法確定一定可以運作。大家多少有類似的經驗,例如使用 map 渲染一個 List,結果在輸入參數為其中一種,但相對應呼叫的物件卻沒有該方法時,就會讓程式意外出現 Error。

接下來,就讓我們來介紹常見的五種型別判斷,並說明使用時機為何。

  1. 使用型別斷言處理聯合型別
  2. 利用型別謂詞(Type Predicates)判定精確型別
  3. 使用 in 來判斷是否為特定屬性或方法
  4. 使用 typeof 簡化型別檢查
  5. 運用 instanceof 處理類別型別




1. 使用型別斷言處理聯合型別

每一個需要使用其屬性的地方,都需要使用斷言來確認其型別。

型別斷言是一種 TypeScript 語法,讓開發者告訴編譯器,用以確定變數的具體型別。例如,我們有一個變數 valuestring | number 型別。

如果 valuestring,就可以使用型別斷言的關鍵字 as,將 value 斷言為 string,例如:

let strValue = value as string;

當使用斷言 as 後,後續就可以安全地使用 string 的方法,例如 toUpperCase() 等。但如果在當下無法判斷是否一定為特定型別,就需要在每一處都寫上 as

實際案例:

// 除非使用斷言,不然就會報錯

function getStarted(vehicle: Car | Airplane): Car | Airplane {
if ((vehicle as Car).drive) {
(vehicle as Car).drive();
} else if ((vehicle as Airplane).fly) {
(vehicle as Airplane).fly();
}
}

因為上述的 vehicle 如果沒有使用斷言,TS 並不確定他一定會有該屬性。因此如果直接使用 vehicle.drive() 就會報錯。




2. 利用型別謂詞判定精確型別

型別謂詞是 TypeScript 中一個高級功能,它允許在函數中定義一個返回值為特定型別謂詞的表達式。這種方法非常適合於實現自定義 Type Guard。可以看到上述的方式,就算使用判斷式讓程式不會出錯,仍舊需要使用大量的斷言,這會導致程式碼難以閱讀。

透過 Type Guards 的型別謂詞(Type Predicates),可以有效優化程式碼的結構。要定義一個 Type Guard,其實只需要使用一個函式來處理即可。他需要返回一個型別謂詞,簡單來說就是返回一個 boolean,藉以確認是否屬於該屬性。

如何定義型別謂詞(Type Predicates)?

返回的 parameterName is Type 這個形式便是型別謂詞,且 parameterName 必須為參數之一。

關鍵在於 vehicle is Car 這一段型別謂詞透過 is 來確定其型別。若返回 true 代表 vehicle 是 Car 型別,TS 便會協助縮減型別;反之則為 Airplane

function isCar(vehicle: Car | Airplane): vehicle is Car {
return (vehicle as Car).drive !== undefined;
}


應用型別謂詞後的差異

透過 isCar 限縮型別,下面的例子便可以順利使用,而不需要使用斷言:

// 'drive' 和 'fly' 都不會報錯了
if (isCar(vehicle)) vehicle.drive();
else vehicle.fly();

需要特別注意,此處因為僅有 CarAirplane,因此才可以使用 else 直接指定為 Airplane 型別,不然如果有三種類型,就一樣需要 else if 來判斷類型,不然 TS 一樣會不知道是哪一種。




3. 使用 in 來判斷是否為特定屬性或方法

若要簡化並確認特定物件中,是否有所屬的屬性或方法,可以使用 in。其使用方法為 n in xn樣板字面值(Template Literal)字串(String),而 x 則為聯合類型(Union Type)

若返回 true,代表其擁有一個必須或可選且存在的特定屬性或方法;若為 false 則為一個可選屬性不存在n 屬性。


實際程式碼

if ("drive" in vehicle) vehicle.drive();
else vehicle.fly();

透過 JavaScript 內建的 in 方法,就能夠判斷該物件裡是否有 drive 屬性,進而提升程式碼的可閱讀性,也避免了必須使用斷言的問題。




4. 使用 typeof 簡化型別檢查

typeof 運算子是 JavaScript 中的一個原生功能,TypeScript 擴展了其用途作為一種簡單的 Type Guard。如果我們要判斷原始型別如:

  • string
  • number
  • Symbol
  • boolean

以上四種類型,可以直接使用 typeof 來進行判斷。且 Type Guard 只會判別以上四種類型,且只有以下兩種形式:

  • typeof T === "number"
  • typeof T !== "number"

當然,我們還是可以和其他的字串比較,但 Type Guard 不會視為判斷基準。結合上面的型別謂詞用法,可以使用如下的形式:

function isString(test: any): test is string {
return typeof test === "string";
}




5. 運用 instanceof 處理類別型別

instanceof 運算子是另一種原生 JavaScript 功能,用於檢查一個對象是否為特定類別或其子類別的實例。在 TypeScript 中,它可以用作 Type Guard,特別是當處理某些類別(class)的實例,但在前端開發的領域因為 class 用的少,因此相較之下也比較少應用到。

class Animal {}
class Dog extends Animal {
bark: () => console.log('汪!');
}
class Cat extends Animal {
meow: () => console.log('喵!');
}

// 透過 instanceof 判斷使用
function makeSound(animal: Animal) {
if (animal instanceof Dog) {
animal.bark();
} else if (animal instanceof Cat) {
animal.meow();
}
}




實踐與應用

透過 Type Guard 的使用,能夠讓 TypeScript 更能夠判斷型別,進而提升程式碼的可維護性與安全性。且另一個優勢,是在撰寫型別判斷時,也能夠主動意識到輸入的型別可能的 Edge Case,進而保護程式碼。

未來因為工作中會需要大量使用 TypeScript,因此希望透過整理文章,讓自己的概念更加清晰。如果有任何建議或想法,都歡迎留言或來信補充!


參考資料 Reference

此處作為整理前端(Frontend)和相關的 HTML、CSS、JavaScript、React 等前端觀念與技巧,全部都會收錄在這個專題之中。同時也會將相關的技術與反思記錄在此,歡迎各位讀者互相交流。
留言0
查看全部
發表第一個留言支持創作者!