Typescript 一些令人又愛又恨的內容 — Type Guard、Narrowing
前言
由於 JavaScript 本身是弱型別語言,因此在開發上很常因為不知道變數的型態是什麼而感到苦惱,即使藉由命名的方式讓變數的定位稍微明確一點,我們還是很難一眼就知道他的型別甚至當此變數是一個 object 時我們更難知道裡面有哪些 key,因此大家漸漸開始使用 TypeScript 作為主要的開發工具。
不曉得大家在利用 TypeScript 進行開發時,有沒有覺得 TypeScript 在檢查型別這塊特別惱人,雖然知道這些型別檢查的舉動是非常好的,可以幫助我們減少許多可能會發生的潛在錯誤,今天筆者就要來談談當我們在開發上遇到這種問題時該如何解決。
情境一
不曉得大家有沒有遇過這種問題,今天想要讓這個變數查看是否符合 enum 中的某一個值,結果 TypeScript 就噴錯給你看了,像下面這樣。
其實要解決上面的紅字方法非常多,首先是開大絕使用 @ts-ignore
讓錯誤消失,當然這個方法非常不好,等於是叫 TypeScript 不要檢查下面這行了。
這時候可能會想到另一個方法,上面的錯誤訊息是說 male
沒有被 assign
到 GENDER
這個 type,所以我只要強制塞給他這個 type 就好,就像這樣:
可是這樣寫仍然不好,等於你強制轉型這個變數了,讓這個變數失去了彈性,接下來筆者就要介紹比較好用的方法了,就讓我們繼續看下去吧!
Type Guard
首先要介紹的是 Type Guard,Type Guard 顧名思義就是型別的看守者,今天 TypeScript 會報錯就是因為 type 不一樣,所以只要我們建立一個型別的看守者,讓 TypeScript 知道這個變數一定會符合我 enum 中的某一個 value 時,這時候就不會出現紅字了,而通常 Type Guard 會寫成一個 function 像這樣:
這時候我們可以發現 gender
這個變數已經從 string
type 變成 GENDER
type 了,所以即便我很無聊的再做一次 includes
的判斷 TypeScript 也不會報任何錯誤了。
這邊筆者在指定 gender
這個值之前先指派這個變數是一個 string type,這個動作很重要,如果沒有先指派變數型態再給值的話這個變數就沒辦法順利改變 type 了。
情境二
不曉得大家有沒有遇過在 API 回傳的資料,也會因為資料對應到的 enum 的值不同而發生錯誤,像下面這樣:
有了上面 Type Guard 的觀念後,這時候的讀者一定知道要寫一個 function 來處理這段錯誤訊息:
的確錯誤訊息沒有了,但很奇怪的是 gender
竟然變成 never
type 了,而這個就是 Type Guard 會做到的一個型別保護機制叫:Narrowing。
Narrowing
Narrowing 翻成白話文就是型別限縮,在 TypeScript 的世界中每一個 enum 基本上都是獨立存在彼此之間是沒有交集的,關係圖就像下面這樣:
所以今天要進行兩個 enum 間的型別轉換就很容易產生出一個可能不會存在的型別,對於可能不會存在的型別 TypeScript 把這個型別定義為 never,而這時候當我們使用了 Type Guard 的技巧,TypeScript 就會自動把型別限縮成 never
type,而不是自動轉換成另一個 enum 了。
當然聰明的你可能會這樣想:那我只要把 function return 定義成另一個 enum 不就好了,這樣就可以確保我 Type Guard 的結果一定會型別轉換成我想要的 enum,像下面這樣:
這樣寫看起來的確沒有什麼問題,我們想要的結果也從型別限縮變成了型別轉換,但這樣做其實就有點不太符合 Type Guard 的精神,畢竟 Type Guard 要做的是型別檢查而不是型別轉換,而且假如我們要做的是型別轉換,這樣寫也會讓這個 function 的複用性不高,因此筆者接下來要介紹比較好的型別轉型方法。
Mapper enum
首先我們可以先想想如何讓型別轉換這件事被複用,我們不妨把想法簡單化,就是建立一個 function 把 A 型態轉換成 B 型態,而這時候就必須要利用 TypeScript 中的 Generics 泛型這個技巧了,像下面這樣:
這個 createEnumMapper
的 function 是一個 currying function,第一個變數傳入的是 enum 本身,這時候 TypeScript 的 Generics 就會知道我的 T
就是跟 enum 本身有關。
為了讓這個 Generics 可以正確的把兩個 enum mapping 起來,我們必須要先建立一個 object 把兩個 enum 的 key value 配對像下面這樣:
由於我們上面的 mapper 是把 enum 的 value 當成 key,所以我們只要帶入 data 的值就可以直接轉換了,像下面這樣:
這時候就可以發現我們成功的把 BE_GENDER
type 的值轉成 FE_GENDER
type 的值了,而且也不需要動用到 Type Guard 的觀念。
小結
今天介紹了 TypeScript 中用來檢查型別的方法,假如讀者日後遇到類似這種問題不妨可以多加利用 Type Guard 進行檢查,而不是直接開大絕用 @ts-ignore
或者 as
這兩種方法,除了介紹型別檢查外也介紹了如何進行型別轉換,希望這些方法都可以讓讀者未來在使用上都不會有太多的問題。
也感謝筆者同事 Peter Lin 提供 createMapper
這套這麼好用的方法可以介紹給大家。