iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 11
1
Modern Web

讓 TypeScript 成為你全端開發的 ACE!系列 第 11

Day 11. 前線維護・特殊型別 X 無法無天 - Any & Unknown Type

https://ithelp.ithome.com.tw/upload/images/20190915/20120614YVpy5Ub57q.png

閱讀本篇文章前,仔細想想看

  1. 當函式遇到 100% 無法跳脫會拋出例外的狀況,這時 TypeScript 會如何對該函式進行推論?
  2. never 型別為所有型別的 Subtype —— 請問這使得 never 擁有什麼特性?never 和別的型別進行複合(unionintersection)會發生什麼事?
  3. 試問 TypeScript 裡最需要避免的狀況有哪些?(答案如果能夠列舉越多,代表讀者越清楚 TypeScript 的雷點)
  4. 試問 TypeScript 裡最需要主動對變數(或函式的參數)作型別註記的時機?(同上,答案如果能夠列舉越多,代表讀者越清楚 TypeScript 的型別推論機制)
  5. 承上題,我們允許讓 TypeScript 對變數作自由地型別推論的時機又有哪些?

前三題都是前一篇文章所解答過後的知識,而後面三題則是考驗目前讀者對於 TypeScript 的型別推論與註記理解到什麼程度。因此呢~後面三題能夠完美地回答出來(不是怪物、天才不然就是經驗豐富的高手),就表示讀者確實把 Day 02 看到 Day 09 的內容全部看完而且有內化進去!

要說筆者能不能 100% 完美解答?其實對筆者來說也很困難,一下子要在短時間內記那麼多 Case 是很難的(所以都是靠經驗久了自然就會注意到)。另外,最好的學習方式除了實作之外,寫文章教別人也是很棒的方式。

[2019.09.15 新增] tsconfig.json 設定

這裡筆者必須緊急說明:若讀者試著筆者舉的程式碼範例的話,請讀者記得將裡面的 strictNullCheck 選項改成 true,這一點忘記在文章系列的一開頭提醒讀者,實在是很抱歉!

/* tsconfig.json */
{
  "compilerOptions": {
    /*  ...  */
    "strictNullChecks": true,
    /* ... */
  }
}

因此請讀者注意,目前學習的 TypeScript 型別系統版本多了一個 strictNullCheck 的編譯器屬性設定!至於為何會造成如此狀況,那是因為筆者在專案上習慣將某些 TypeScript 編譯器設定啟動!至於 strictNullCheck 到底為何,將會在型別系統講述告一段落後,開始講述 TypeScript 的編譯器設定檔喔!

看來今天是《前線維護》篇章系列,看起來比較有 BOSS 級別的感覺啊。讀者準備好進行討伐後,那麼我們就...

正文開始

前線維護・終章:any & unknown 型別

這邊讀者還是得注意一點,未來 TypeScript 說不定還會再出更多型別(但筆者也想不到會是什麼)。但就目前來說,TypeScript 第三版後出現的 unknown 型別,已經算是最新的 Feature,因此這裡將 unknown 視為本篇章的最後一道關卡。

預防 any 型別

看到這裡,筆者必須提醒 —— 真的非不得已狀態下或者是快速測試下,可以使用 any。不過開發上,儘量不要用到 any 比較好;或者真不巧,遇到 any,也應當主動註記

從本系列文章的開頭就講了:

any 會造成 TypeScript 跳過檢測使得變數容易引發非預期行為的機率完全上升了

沒什麼特殊的重點,但就是得列出我們從這系列文章得知,any 可能會在哪些情況出現:

重點 1. any 出現的時機

  1. 遲滯性指派 Delayed Iniitialization:變數定義時,除了未加註記(Type Annotation)外,也沒有指派值或者被指派為 Nullable Types。(參照 Day 02.
  2. 一般宣告下的函式參數:一般被宣告的函式,其參數通常會直接被推論為 any,又被稱作 Implicit any 的情形。此狀況是少數會被 TypeScript 主動通報的(參照 Day 04.
  3. 函式回傳之值:有些實務上,型別無法確定,因此到最後只能將回傳值預設為 any(如:JSON.parse)(參照Day 04.
  4. 未註記之空陣列:沒有積極型別註記到的空陣列,其預設推論為 any[] (參照 Day 05.
  5. 跟 I/O 行為有關:例如,從外部 CSV 檔案讀取表格行格式(通常用陣列或元組型別),若沒有特殊註記的話,通常會用 any 作表示(這可能是讀者少數會主動用 any 的狀況)(參照 Day 06.

以及 ...

  1. 其他筆者沒有想到的狀況 XD(重點的大部分範圍都涵蓋在前五點喔

看到這裡,讀者應該可以確保自己不誤入到 TypeScript 的 any 下的陷阱吧!當然,接下來又是挺整人的時刻~

unknown 型別的機制探討

不過研究過程中,筆者意外發現,unknown 的機制挺不錯!就讓筆者給大家看看官方 TypeScript 3.0 Unknown Type 是怎麼說的:

TypeScript 3.0 introduces a new top type unknown. unknown is the type-safe counterpart of any. Anything is assignable to unknown, but unknown isn’t assignable to anything but itself and any without a type assertion or a control flow based narrowing. Likewise, no operations are permitted on an unknown without first asserting or narrowing to a more specific type.

這裡為了讓讀者好理解,我們就拆成兩段。

第一段:unknown 相對 any 來說,是一種更安全的型別機制(a type-safe counterpart)

unknown is the type-safe counterpart of any. Anything is assignable to unknown, but unknown isn’t assignable to anything but itself and any without a type assertion or a control flow based narrowing.

第一句話我們就略過,因為被筆者標註在上面 XD —— unknown 是更安全的型別機制。

今天的目標就是要理解:為何 unknown 相對 any 來說,型別上的使用更加安全?

第一段中的第二句話超長,我們先來看前半部分:

“Anything is assignable to unknown

unknownany 的共通點是:只要當變數被註記為 anyunknown,該變數照樣都可以接收任意型別的值。(檢測結果如圖一)

https://ithelp.ithome.com.tw/upload/images/20190915/201206149jY4Gb6vUv.png

https://ithelp.ithome.com.tw/upload/images/20190915/20120614ncMy2f4Qgb.png
圖一:anyunknown 之間註記在變數上,被指派任意值都沒差

好的,這裡應該沒問題,不過可能讀者會想說:“不是都說 unknownany 安全嗎!?安全兩字在哪!?”(拍桌)

關鍵點在後半段這句:

“but unknown isn’t assignable to anything but itself and any without a type assertion or a control flow based narrowing.”

我們來試試看下面這個例子。(使用 any 指派到任何型別的變數之檢測結果如圖二,而使用 unknown 型別的變數指派則是如圖三)

https://ithelp.ithome.com.tw/upload/images/20190915/20120614bhLDpikDnc.png

https://ithelp.ithome.com.tw/upload/images/20190915/20120614MTnezwOVAb.png
圖二:any 型別跟我們預期的一樣,就是一個廢廢的型別

https://ithelp.ithome.com.tw/upload/images/20190915/20120614CfQ9f5CuN8.png
圖三:哦?unknown 型別的值不能被強行指派到 —— 除了 anyunknown 型別外 —— 的任意型別變數

讀者應該可以看出一些 unknown 型別的好處,筆者也搶先補充一個適合使用 unknown 的情境:

如果根據 any 可能出現的時機(本篇文章重點 1.)之第 5 點,也就是無法預測的 I/O 行為。開發者可以開發比較不安全版本的讀取 CSV 檔案的函式,也就是回傳 any 型別的格式。

然而,開發者也可以開發安全版本的讀取 CSV 檔案的函式,其回傳的型別為 unknown —— 代表只要任何開發者使用這個安全版本的函式回傳之值,使用者必須強行註記該 CSV 回傳值之格式。就算開發者忘記要註記結果,也會被 TypeScript 主動警告。

另一個直截了當可以馬上使用 unknown 的時機,就是寫一個安全的函式(或方法)把不安全的函式(或方法)包裝起來。比如說,把 JSON.parse 這種會回傳 any 的方法函式包裝起來,變成:

https://ithelp.ithome.com.tw/upload/images/20190915/20120614GMwXNq7Pw3.png

後面會再以這個例子讓大家知道 unknown 的好處,請繼續看下去!

不過在我們進到下一部分前,筆者還沒講完這部分,因為還有一些東西還沒講完:

“but unknown isn’t assignable to anything but itself and any without a type assertion or a control flow based narrowing.”

把講過的部分去除並簡化成:

“but unknown isn’t assignable to anything but control flow based narrowing.”

讀者看到這一段知道在講什麼嗎?

“control flow based narrowing”

其實它的概念是 —— 只要程式根據判斷式與敘述式的結構,縮小變數在型別推論上的範疇,我們就可以讓純 unknown 型別的變數被指派到任意型別上。不解釋直接先看下方的例子(檢測結果如圖四):

https://ithelp.ithome.com.tw/upload/images/20190915/20120614rIZqeAiSgg.png

https://ithelp.ithome.com.tw/upload/images/20190915/20120614mGFLbakemI.png
圖四:結果我們藉由所謂的型別系統的一種技巧 -- Type Guard 限制型別被推論到的可能性 —— 來 Bypass unknown 型別原先的限制 —— 不能被指派到被註記到的任意型別(除了 unknownany)的變數

貼心小提示

這是讀者第一次在本系列看到所謂的型別限縮的技巧,又被稱為 Type Guard,但個人覺得適合的翻譯應該是『 型別檢測 』,不過筆者還是會稱它為型別限縮的技巧

本技巧的一些細節將會跟複合型別(unionintersection)一起講到(Day 17.)。簡單知道過後就讓筆者繼續回歸本日的主題。

以上的例子,一開始直接把 isUnknown 指派到 number 型別的 isNumber 變數裡,理所當然會出現紅色的警告線,被 TypeScript 警告。

但是藉由簡單的 if...else... 敘述式,isUnknown 的型別推論限縮到 number 型別 -- 因此TypeScript 可以根據這樣的結構斷定在這控制結構裡面的 isUnknown 必為 number 型別!這就是為何 isUnknown 在型別推論的限縮下仍然可以安全地被指派到 isNumber 變數裡(被指派到其他型別的變數)。

還有另一種方式可以將 unknown 型別的值指派到一般註記的變數裡,就是用顯性的型別註記。(Explicit Type Annotation)但是讀者早就看過了這種做法。

不過這裡的運用就是開發者必須很確定自己到底在做什麼,才會跟 TypeScript 講說,這個 unknown 型別的變數實質上是某某型別(檢測結果如圖五):

https://ithelp.ithome.com.tw/upload/images/20190915/20120614thkSgZIBEV.png

https://ithelp.ithome.com.tw/upload/images/20190915/20120614melxiBQS0h.png
圖五:強制將型別註記限制在 unknown 型別上

哇,簡簡單單的 unknown 型別就這樣被轉型了!要是人生能夠自由快速地轉型就真的好輕鬆啊~

重點 2. unknown 型別下的變數指派限制

  1. any 型別相似的地方在於:若變數被 unknown 型別註記,則該變數可以被任意型別的值指派
  2. 若被註記為 unknown 型別的變數,除了以下情形外,否則不得將其值指派到任意型別撇除 unknownany 型別的變數裡:
    • 顯性註記之型別 T 等同於被指派到的變數之型別 T
    • 根據程式的控制流程分析,其 unknown 型別的推論被限縮到特定的型別 U 致使可以被指派到其他型別 U 的變數

第二段:變數若具備 unknown 型別則不能輕舉妄動

這個副標題其實就已經把第二句話,也就是以下這句話講得清清楚楚的:

“no operations are permitted on an unknown without first asserting or narrowing to a more specific type.”

第二段在探討的不是 unknown 型態的指派行為(那是剛剛第一段在探討的)。讀者可以這麼想:

unknown 型態的變數,基本上被鎖住不能使用了

讀者看到想說:“搞毛啊!不就變唯讀的感覺嗎?”。

要讀取該變數確實是可以讀的(用 console.log 並且編譯過後執行 XD)。除此之外,被註記為 unknown 的變數什麼事情的不能做!除非開發者對該變數進行顯性的型別註記亦或者根據程式碼的控制流程分析判斷說該變數的型別被限縮到某個範疇,該變數才有機會做一些事情。(比如:呼叫屬性、方法等)

等等,這跟第一段講述的差異在哪?不也是顯性註記過後,亦或者控制流程分析過後限縮型別 -- 以上兩種情形嗎?

注意第一段跟第二段探討的重點差異:

第一段:unknown is the type-safe counterpart of any. Anything is assignable to unknown, but unknown isn’t assignable to anything but itself and any without a type assertion or a control flow based narrowing.

第一段在描述 unknown 型別變數指派的機制(Assignment)

比較:

第二段:“no operations are permitted on an unknown without first asserting or narrowing to a more specific type.”

第二段針對 unknown 型別可以做的事情:重點在探討 unknown 型別的『行動範圍』(Operation)

也就是說,unknown 型別的變數基本上一丁點事情都沒辦法做,除非我們刻意跟 TypeScript 講說它是哪一種型別,亦或者是根據控制流程分析限縮型別的範疇讓 TypeScript 推斷說該變數確切的型別是什麼(運用上一部分講述的型別限縮的技巧 -- Type Guard)。

以下舉例(推論結果為圖六):

https://ithelp.ithome.com.tw/upload/images/20190915/20120614kT67tQrDo7.png

https://ithelp.ithome.com.tw/upload/images/20190915/20120614WqfRLJsXPP.png
圖六:結果 any 型別 TypeScript 不會去理會,但 unknown 的行動完全被鎖住

你可以看到以上的結果:any 型別不管亂呼叫什麼東西,都不會有事。

然而 unknown 型別就不同啦~ 只要亂動 unknown 型別,TypeScript 就會跟你說:“該變數存的物件是 unknown,因此你不能對它做任何事情或呼叫任何方法”。(錯誤訊息如圖七)

https://ithelp.ithome.com.tw/upload/images/20190915/20120614jmxa8zEqEa.png
圖七:所以 TypeScript 會幫助我們鎖定 unknown 型別的行動

要使得 unknown 型別是可以被啟動的,我們可以試試看下面的範例。(顯性型別註記為圖八,而型別限縮為圖九)

https://ithelp.ithome.com.tw/upload/images/20190915/20120614g7LJOcfjCy.png

https://ithelp.ithome.com.tw/upload/images/20190915/20120614CXamH2kxAY.png
圖八:針對 unknown 型別作型別註記即可立即使用

https://ithelp.ithome.com.tw/upload/images/20190915/20120614DfQEQWh7dI.png
圖九:編譯器自行判斷該 unknown 型別確定被推論到某種型別後就即可使用

重點 3. unknown 型別變數的特性

假設某變數 A 被指派為 unknown 型別,則:

  1. A 不能呼叫任何方法或屬性,亦不可作為任何函式或方法之參數
  2. A 不能指派到型別為 T 的變數,其中 T 不為 unknownany 型別 (根據本篇重點 2.)
  3. A 被顯性地型別註記為某型別 T(其中 T 不為 unknown),則 A 可以作為該型別 T 之代表值,進行該型別底下合理之操作
  4. A 被控制流程限縮型別至某型別 T(其中 T 不為 unknown),則 A 可以作為該型別 T 之代表值,在該控制流程的範圍內進行合理之操作

在討論第一段的後面,筆者提到可以寫一個安全的函式(或方法)把不安全的函式(或方法)包裝起來。比如說,把 JSON.parse 這種會回傳 any 的方法函式包裝起來。這樣的好處就由以下程式碼來進行驗證吧!(驗證結果為圖十,錯誤訊息為圖十一)

https://ithelp.ithome.com.tw/upload/images/20190915/20120614WrBILV5vrA.png

https://ithelp.ithome.com.tw/upload/images/20190915/20120614vKbKcV5gm5.png
圖十:直接使用 JSON.parse 會回傳 any;然而,使用安全的 unknown 回傳型別的函式會自動讓變數註記成 unknown,每次回傳的值必須被強行註記才可使用

https://ithelp.ithome.com.tw/upload/images/20190915/20120614ByD8nYmPg4.png
圖十一:TypeScript 確保我們不會隨隨便便亂用變數的屬性,實在是很聰明的~

從剛剛的範例可以知道,筆者將 JSON.parsesafelyParseJSON 這個函式包裝後,只要將不明的 JSON 物件解析出來,就一定要遵照使用 unknown 變數的原則:必須顯性地註記才能使用。因此 unknown 可以協助我們解決本篇重點 1 談到的 any 的出現狀況的第 3 點和第 5 點的案例呢!

unknown 型別的複合特性

哇!我們還沒討論完啊?

快完成囉~這邊討論的概念跟 never 很像,但是又比 never 稍嫌麻煩一些。

根據已知 unknown 的特性:只要我們對 unknown 型別的變數作顯性型別註記,該型別就會取代 unknown 的狀態!

運用複合型別的概念反過來推論:任何型別只要和 unknown 交集(intersection)在一起,unknown 就會被任意型別吸收

例如:如果 number 型別跟 unknown 型別 intersect 在一起,該變數要同時為 number 也同時為 unknown 型別的話,那就只會有一種狀況 —— 該變數本身就是 number 型別。

因此,以下的程式碼推論出來的結果,都會將 unknown 型別給吸收掉。(圖十二為官方的範例程式碼截圖,其中,所有的 unknown 都被其他型別藉由 intersection 被吸收掉了)

https://ithelp.ithome.com.tw/upload/images/20190915/20120614B5cwnMhIX4.png
圖十二:unknown 跟任何型別進行 intersection 時,就會被該型別吸收掉

任何型別如果可以同時是 —— 比如 number 型別或 unknown 型別,那這樣的狀況是不是變得不確定了?『 同時 』的概念跟 union 很像。因此 unknownnumber 進行 union 時,反而是 unknown 吸收掉 union 型別。

但是呢,如果 unknown 型別跟 any 型別進行 union 可就不同囉,要可以同時是 unknown 或者是 any —— 由於 any 的自由程度大過於 unknown,因此這裡的邏輯推斷反而是 any 會把 unknown 型別吸收掉。(圖十三為官方的範例程式碼截圖,所有的 unknown 吸收掉所有跟它 union 的型別,除了 any

https://ithelp.ithome.com.tw/upload/images/20190915/20120614zSZiWSnkUH.png
圖十三:unknown 跟任何型別(除了 any) 進行 union 後,會吸收掉該型別

重點 4. unknown 型別進行複合的作用

  1. 任何與 unknown 型別進行 intersection 過後的型別 T,則 T 會吸收掉 unknown
type U = unknown & T;
// => T
  1. 任何與 unknown 型別進行 union 過後的型別 T,且 T 不為 any 型別,則 unknown 會吸收掉 T
type U = unknown | T;
// => unknown

type V = unknown | any;
// => any

小結

筆者寫到這裡,也把整個型別系統大部分都講完了,包含:

  • TypeScript 的型別推論(Type Inference)與註記(Type Annotation)的定義
  • 型別推論在原始型別、廣義物件型別、明文型別、TypeScript 內建型別、特殊型別的型別推論與註記機制
  • 各種型別情境下,使用型別推論以及型別註記的適當時機

相信讀者讀到這裡也了解了大部分的 TypeScript 型別系統的機制。不過筆者承認,這應該是本系列枯燥乏味的部分。然而,不理解這裡面細微的機制與作用(還有潛在的雷)會造成開發過程很痛苦啊。因此筆者也不敢草草帶過這個部分,才儘量寫得不要太像在讀 Document 而是有推斷過後、循序漸進的感覺。

而昨天和今天所探討的 never 以及 unknown 這兩個特殊型別的機制之所以要從官方文件著手的理由 —— 它不算是可以靠邏輯推論得知的行為,而是 TypeScript 本身的 Feature 啊!既然很難推論的話,這時候才是讀 Doc 好時機。


上一篇
Day 10. 前線維護・特殊型別 X 永無止盡 - Never Type
下一篇
Day 12. 機動藍圖・介面宣告 X 使用介面 - TypeScript Interface Intro.
系列文
讓 TypeScript 成為你全端開發的 ACE!51

尚未有邦友留言

立即登入留言