Typescript 一些令人又愛又恨的內容 — Type Guard、Narrowing

前端小智發表於2022-03-01
作者: 神Q超人
譯者:前端小智
來源:medium

有夢想,有乾貨,微信搜尋 【大遷世界】 關注這個在凌晨還在刷碗的刷碗智。

本文 GitHub https://github.com/qq449245884/xiaozhi 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

由於 JavaScript 本身是弱語言,因此在開發上常因為不知道變數的型別是什麼而感到苦惱,即使藉由命名的方式讓變數的定位稍微明確一點,我們還是很難一眼就知道他的型別甚至當此變數是一個 object 時我們更難知道里面有哪些 key,因此大家漸漸開始使用 TypeScript 作為主要的開發工具。

不曉得大家在利用 TypeScript 進行開發時,有沒有覺得 TypeScript 在檢查型別這塊特別惱人,雖然知道這些型別檢查的舉動是非常好的,可以幫助我們減少許多可能會發生的潛在錯誤,今天就要來談談當我們在開發上遇到這種問題時該如何解決。

場景一

不曉得大家有沒有遇過這種問題,今天想要讓這個變數檢視是否符合 enum 中的某一個值,結果 TypeScript 就噴錯給你看了,像下面這樣。

image.png

其實要解決上面的紅字方法非常多,首先是開大絕使用 @ts-ignore 讓錯誤消失,當然這個方法非常不好,等於是叫 TypeScript 不要檢查下面這行了。

image.png

這時候可能會想到另一個方法,上面的錯誤資訊是說 male 沒有被 assignGENDER 這個 type,所以我只要強制塞給他這個 type 就好,就像這樣:

image.png

可是這樣寫仍然不好,等於你強制轉變這個變數了,讓這個變數失去了彈性,接下來我們介紹比較好用的方法,就讓我們繼續看下去吧!

Type Guard

首先要介紹的是 Type Guard,Type Guard 顧名思義就是型別的看守者,剛剛 TypeScript 會報錯就是因為 type 不一樣,所以只要我們建立一個型別的看守者,讓 TypeScript 知道這個變數一定會符合我 enum 中的某一個 value 時,這時候就不會出現紅字了,而通常 Type Guard 會寫成一個 function 像這樣:

const assertsIsGender = (gender: any) : gender is GENDER => {
  return Object.values(GENDER).includes(gender)
}

這時候我們可以發現 gender 這個變數已經從 string type 變成 GENDER type 了,所以即便我很無聊的再做一次 includes 的判斷 TypeScript 也不會報任何錯誤了。

這邊我在指定 gender 這個值之前先指派這個變數是一個 string type,這個動作很重要,如果沒有先指派變數型別再給值的話這個變數就沒辦法順利改變 type 了。

image.png

場景二

不曉得大家有沒有遇過在 API 回傳的資料,也會因為資料對應到的 enum 的值不同而發生錯誤,像下面這樣:

image.png

有了上面 Type Guard 的觀念後,這時候的讀者一定知道要寫一個 function 來處理這段錯誤資訊:

image.png

的確錯誤資訊沒有了,但很奇怪的是 gender 竟然變成 never type 了,而這個就是 Type Guard 會做到的一個型別保護機制叫:Narrowing

型別收窄(Narrowing)

Narrowing 翻成白話文就是型別收窄,在 TypeScript 的世界中每一個 enum 基本上都是獨立存在彼此之間是沒有交集的,關係圖就像下面這樣:

image.png

所以要進行兩個 enum 間的型別轉換就很容易產生出一個可能不會存在的型別,對於可能不會存在的類別 TypeScript 把這個型別定義為 never,而這時候當我們使用了 Type Guard 的技巧,TypeScript 就會自動把型別收窄成 never type,而不是自動轉換成另一個 enum 了。

當然聰明的你可能會這樣想:那我只要把 function return 定義成另一個 enum 不就好了,這樣就可以確保我 Type Guard 的結果一定會型別轉換成我想要的 enum,像下面這樣:

image.png

這樣寫看起來的確沒有什麼問題,我們想要的結果也從型別收窄變成了型別轉換,但這樣做其實就有點不太符合 Type Guard 的精神,畢竟 Type Guard 要做的是型別檢查而不是型別轉換,而且假如我們要做的是型別轉換,這樣寫也會讓這個 function 的複用性不高,因此我們接下來要介紹比較好的型別轉型方法。

Mapper enum

首先我們可以先想想如何讓型別轉換這件事被複用,我們不妨把想法簡單化,就是建立一個 functionA 型態轉換成 B 型態,而這時候就必須要利用 TypeScript 中的 Generics 泛型這個技巧了,像下面這樣:

const createEnumMapper = <T>(mapping: T) => (value: keyof T | null) : T[keyof T] | undefined => {
  return value === null ? undefined : mapping[value]
}

這個 createEnumMapper 的 function 是一個 currying function,第一個變數傳入的是 enum 本身,這時候 TypeScript 的 Generics 就會知道我的 T 就是跟 enum 本身有關。

為了讓這個 Generics 可以正確的把兩個 enum mapping 起來,我們必須要先建立一個 object 把兩個 enum key value 配對像下面這樣:


const mapper = {
  [BE_GENDER.MALE]: FE_GENDER.MALE,
  [BE_GENDER.FEMALE]: FE_GENDER.FEMALE
}

由於我們上面的 mapper 是把 enum 的 value 當成 key,所以我們只要帶入 data 的值就可以直接轉換了,像下面這樣:

image.png

這時候就可以發現我們成功的把 BE_GENDER type 的值轉成 FE_GENDER type 的值了,而且也不需要動用到 Type Guard 的觀念。

總結

今天介紹了 TypeScript 中用來檢查型別的方法,假如讀者日後遇到類似這種問題不妨可以多加利用 Type Guard 進行檢查,而不是直接開大絕用 @ts-ignore 或者 as 這兩種方法,除了介紹型別檢查外也介紹瞭如何進行型別轉換,希望這些方法都可以讓讀者未來在使用上都不會有太多的問題。

我是刷碗智,新的一年,我們一起刷刷刷。


程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug

https://medium.com/onedegree-...

交流

有夢想,有乾貨,微信搜尋 【大遷世界】 關注這個在凌晨還在刷碗的刷碗智。

本文 GitHub https://github.com/qq44924588... 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

相關文章