會希望寫這篇文章,是有鑑於在學習 TypeScript 後,很常遇到在參數可以輸入多個型別,但卻可能會使用到不同的方法或屬性時,總是會遇到各種困難。為了了解並避免未來遇到這種狀況時,變成一位 any
工程師,因此先寫下這篇文章以供參考。
就讓我們開始來了解,TypeScript 中五種常見的型別防禦方式吧!
在撰寫 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;
}
上述的例子是,我們有 Car
和 Airplane
兩個函式,我們各自希望他在 getStart
被呼叫時,可以啟動各自的方法。
那這和 Type Guard 有什麼關係?而之所以需要處理 Type Guard 的原因,就是因為我們的參數 vehicle
有兩種,因此需要特別進行判斷。
開始說明前,也簡單說明 TypeScript 的聯合型別是什麼。
聯合型別(Union Type)允許一個變數屬於多種型別之一,提供了靈活處理不同型別值的能力。例如,let myVar: string | number;
聲明了一個變數 myVar
,它可以存儲字符串或數字型別的值。這在處理如使用者輸入、API 資料回傳等中特別有用,讓我們能夠在限縮變數的型別時,又保留了一些彈性。
但也因為參數的不確定,導致函式內部的方法也無法確定一定可以運作。大家多少有類似的經驗,例如使用 map
渲染一個 List,結果在輸入參數為其中一種,但相對應呼叫的物件卻沒有該方法時,就會讓程式意外出現 Error。
接下來,就讓我們來介紹常見的五種型別判斷,並說明使用時機為何。
每一個需要使用其屬性的地方,都需要使用斷言來確認其型別。
型別斷言是一種 TypeScript 語法,讓開發者告訴編譯器,用以確定變數的具體型別。例如,我們有一個變數 value
是 string | number
型別。
如果 value
是 string
,就可以使用型別斷言的關鍵字 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()
就會報錯。
型別謂詞是 TypeScript 中一個高級功能,它允許在函數中定義一個返回值為特定型別謂詞的表達式。這種方法非常適合於實現自定義 Type Guard。可以看到上述的方式,就算使用判斷式讓程式不會出錯,仍舊需要使用大量的斷言,這會導致程式碼難以閱讀。
透過 Type Guards 的型別謂詞(Type Predicates),可以有效優化程式碼的結構。要定義一個 Type Guard,其實只需要使用一個函式來處理即可。他需要返回一個型別謂詞,簡單來說就是返回一個 boolean
,藉以確認是否屬於該屬性。
返回的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();
需要特別注意,此處因為僅有 Car
和 Airplane
,因此才可以使用 else
直接指定為 Airplane
型別,不然如果有三種類型,就一樣需要 else if
來判斷類型,不然 TS 一樣會不知道是哪一種。
若要簡化並確認特定物件中,是否有所屬的屬性或方法,可以使用 in
。其使用方法為 n in x
,n
為樣板字面值(Template Literal)或字串(String),而 x
則為聯合類型(Union Type)。
若返回 true
,代表其擁有一個必須或可選且存在的特定屬性或方法;若為 false
則為一個可選屬性或不存在的 n
屬性。
if ("drive" in vehicle) vehicle.drive();
else vehicle.fly();
透過 JavaScript 內建的 in
方法,就能夠判斷該物件裡是否有 drive
屬性,進而提升程式碼的可閱讀性,也避免了必須使用斷言的問題。
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";
}
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,因此希望透過整理文章,讓自己的概念更加清晰。如果有任何建議或想法,都歡迎留言或來信補充!