iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 10
0
Modern Web

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

Day 10. 前線維護・特殊型別 X 永無止盡 - Never Type

https://ithelp.ithome.com.tw/upload/images/20190915/201206146qJyXh3QSG.png

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

  1. 如何跳脫死板定義的型別 type 格式?
  2. 對狹義物件的屬性下達 undefined 之原始型別跟使用選用屬性(Optional Properties)的差別在哪?
  3. 如何快速查找你所定義過的型別化名背後所代表的結構在 TypeScript 裡的程式碼的位置呢?

如果還沒理解完畢的話,可以先翻看前一篇文章喔!

[2019.09.15 新增] tsconfig.json 設定

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

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

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

[2019.09.18 新增] 程式碼範例

如果想要看到本系列文裡面舉的程式碼範例可以參考 Maxwell-Alexius/Iron-Man-Competition 這個 GitHub Repo 喔~寫作過程當中會不斷更新的!

今天的文章應該挺整人的,筆者甚至在寫作前完全低估了 never 型別的機制,因此廢話不多說:

正文開始

特殊型別的運作機制

never 型別的意義

先講特殊型別中相對不棘手的東西:那就是 TypeScript 2.0 提出的 never 型別。

(啊!?難道還有更棘手的?對,你能夠感覺得到 any v.s. unknown 這兩個東西的差別嗎?)

never 型別這東西呢,其實要講說是型別其實也感覺不太像,更精確來說是一種函式或方法回傳值的狀況,就跟 void 很像:

void 型別代表的概念是函式或方法不回傳值的情形

我們就切入 never 的意義吧!

重點 1. never 型別的意義

never 型別的概念是程序在函式或方法執行時:

  1. 無法跳脫出該函式或方法
  2. 出現例外結果中斷執行

好的,通常讀者看到無法跳脫出該函式或方法,想到的是無窮迴圈,因此我們來試試看這一個範例:(其推論結果如圖一)

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

https://ithelp.ithome.com.tw/upload/images/20190915/201206142Lpn0xbdIo.png
圖一:結果 TypeScript 將其推論結果為 () => never

哦!感覺 TypeScript 似乎知道我們想要做什麼,那麼如果是以下的這些狀況呢?(推論結果分別如圖二~四)

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

https://ithelp.ithome.com.tw/upload/images/20190915/20120614BkGySJ6s36.png
圖二:恩!?怎麼是 (number) => void,至少應該也要是 void | never 吧!

https://ithelp.ithome.com.tw/upload/images/20190915/20120614oJA6YKEpJV.png
圖三:恩!?我們已經確保它會出現無窮迴圈的狀況,回傳型別應該是 never 才對吧!

https://ithelp.ithome.com.tw/upload/images/20190915/20120614zEDygaQSIK.png
圖四:反正不管怎樣回傳就是 void 就對了,TypeScript 感覺好像不想理人

詳細解釋 never 型別的機制

筆者一開始還認為,是不是 TypeScript 的型別系統對於這一些案例是無法推論出來的 -- 也就是迴圈無法跳脫的狀態、亦或者是例外拋出的狀況。因此呢:

“如果遇到有可能出現例外狀況,比如有機率被困在迴圈裡或者是被丟例外引發程式中斷,必須要進行積極註記,尤其如果是有回傳值之下,可以使用 union 來和 never 進行複合型別的動作”

如果讀者也跟著這麼想的話,那麼你就踏入了 TypeScript 的陷阱啦!

我們趕快矯正過來。

因此,筆者這邊直接原封不動地祭出 TypeScript 2.0 更新 never 型別的官方訊息,其中的這一段:

The never type has the following characteristics:

  • 第一點:never is a subtype of and assignable to every type.
  • 第二點:No type is a subtype of or assignable to never (except never itself).
  • 第三點:In a function expression or arrow function with no return type annotation, if the function has no return statements, or only return statements with expressions of type never, and if the end point of the function is not reachable (as determined by control flow analysis), the inferred return type for the function is never.
  • 第四點:In a function with an explicit never return type annotation, all return statements (if any) must have expressions of type never and the end point of the function must not be reachable.

筆者認為第一點超重要,這解釋了剛剛的範例為何會出現很奇特的現象:

第一點:never is a subtype of and assignable to every type

其中,subtype 這個單字的出現影射出 TypeScript 有型別的等地制度(Hierarchy)。這是什麼意思呢?如果以 Day 03. 討論的 object 這個型別,我們可以對被 object 型別註記過後的變數指派任何廣義物件(包含狹義物件、函式、陣列、類別以及類別創建的物件等等)。

也就是說,object 這個型別就包含了狹義物件的明文型別、函式、陣列型別、類別等等 -- 因此可以看成這些東西為 object 的 subtype。(不過實際的 TypeScript 官方文件上,並沒有寫說這些廣義物件並非 object 的 subtype,只是被筆者測試過後,認為有相似的行為罷了)

好的,那剛剛我們看到的第一點 -- never 型別為所有型別的 subtype。也就是說 never 是所有型別包含的其中一種情況。配合 never 通常都用在代表函式或方法的回傳值的意義,我們應該可以這麼解讀:

任何函式或方法回傳值都有包含回傳不了值甚至例外狀況的考量

讀者可能還是會覺得上面那句話很模糊,筆者也是很難找到一個很完整的話描述,因此我們來舉另一個例子。

前面說過,never 型別除了是指函式或方法永遠不回傳值(也就是程序會被困在裡面)的狀況外,另一種是出現例外狀況導致程序執行中斷。另外,筆者這邊用複合型別 union 的概念:假如某函式會有回傳值,但也同時出現例外被拋出之狀況的可能性,我們就應該把函式應當回傳值的型別和 never 進行 union(結果如圖五):

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

https://ithelp.ithome.com.tw/upload/images/20190915/20120614zsVP2XK7h5.png
圖五:結果 TypeScript 推論出來的型別,numbernever 給吸收掉了!

哇塞,這時候 never 還故意給我們搞了個隱身術!

其實不是這樣的,我們先看一下,這個寫法:

type EitherNumberOrNever = number | never;

事實上等效於:

type EitherNumberOrNever = number;

https://ithelp.ithome.com.tw/upload/images/20190915/20120614FjUhI3KtZX.png
圖六:numbernever 情形給吸收掉了

有些讀者可能隱隱約約猜到了到底發生什麼事情。never 既然是任何型別的 subtype,也就是說,就算你把包含過 never 型別的概念的東西再對 never 本身重複進行 union -- 由於任何型別早就涵蓋 never 這個案件了,因此自然而然就會吸收掉 never 型別。我們再重新看這一句話仔細想想為何要把 never 設定成任何型別的 subtype:

任何函式或方法回傳值都有包含回傳不了值甚至例外狀況的考量

意義上,好像開始明朗起來,never 的出現重點在於要讓函式或方法的回傳值之型別也必須涵蓋有可能會沒辦法跳脫或者甚至中斷執行的狀況。畢竟我們無法寫出 100% 不會出錯的程式碼(這點已經在 Day 04. 的某個部份講過了)。

讀者試試看

那你能不能夠用剛剛的想法推斷一下,如果是:

type T = void | never;

也會是同樣的結果嗎?也就是 void 會把 never 這個型別也會吸收掉嗎?

那麼以下這種情況呢?

type U = any | never;

那我們就接下去看第二點:

  • 第二點:No type is a subtype of or assignable to never (except never itself).

任何型別除了 never 本身以外,皆不是 never 的 subtype。跟第一點來做比較:number 型別有涵蓋 never 這個 Case;但根據第二點講述的方式:never 型別本身不屬於任何型別,也就是說,never 並沒有涵蓋 number 型別的 Case。

其實這不難理解,若某函式的輸出結果被註記為 never,則代表呼叫該函式時,內部的程式碼必定都會出錯(或被困住),難道還會有回傳任何值的可能嗎?因此呢,藉由推斷,第二點的這段邏輯跟第一點講述的是相對情形

那我們就進行下一個推論:既然第一點跟第二點都是相對概念了,那麼跟第一點談到任何跟 never 型別 union 過後結果都是 never 型別都被吸收的情形。那跟第一點相對的狀況是什麼呢?

有些讀者一定想到:“啊!好像是 never 型別跟任何型別進行 intersection 的狀況!”

沒錯,如果你嘗試這段程式碼,我們還真的看到 TypeScript 推論出 never(推論結果如圖七):

type MustBeNever = number & never;

https://ithelp.ithome.com.tw/upload/images/20190915/20120614dd1ORbakbX.png
圖七:如果一個型別同時是 number 也是 never,那想當然,都已經錯誤了,怎麼會有型別值呢?

看來 never 真是一個神奇的玩意兒(Day 17. 會再度提到,不過是擴展的形式)~

重點 2. never 型別為所有型別的 Subtype

任何型別 T(包含 never 本身)和 never 進行 union,則型別 T 會吸收掉 never 型別:

type WontBeNever = T | never;
// => WontBeNever: T

任何型別 U(包含 never 本身)和 never 進行 intersection,則型別 U 會被 never 型別強行覆蓋:

type MustBeNever = U & never;
// => MustBeNever: never

讀者可能以為筆者會再繼續講下去,但在這之前,我們還有一點點東西還沒釐清楚,我們先回去看一下第一、二點的敘述。

  • 第一點:never is a subtype of and assignable to every type.
  • 第二點:No type is a subtype of or assignable to never (except never itself).

還有另一個單字名為 assignable(可被指派性),其中第一點:

never is a subtype of and assignable to every type

理解起來很簡單,因為 never 既然是任何型別的都涵蓋到的狀況,也可以這麼說:任何函式或方法就算有回傳值,都會有可能出錯的狀況

因此,如果在函式或方法既有回傳值狀況下丟出例外,我們稱之為 never 取代了該回傳值型別的狀態,而變數在 TypeScript 裡也可以被指派這種 never 型別的情況

“WTx,這又是什麼東西!?作者你到底是在講什麼?”(所以筆者今天才說這一篇是整人文,撐一下啊!)

還記得 TypeScript 會對於被註記過後的變數 -- 該變數被指派某些值時,TypeScript 會進行型別檢查吧!我們來看下面這個範例:

https://ithelp.ithome.com.tw/upload/images/20190915/201206145t4Kjc8Yzk.png

上面的範例,我們定義一個變數名為 acceptsNever 對其作積極註記為 number 型別,給它指派 probablyThrowsError(-5) 的結果。

眼尖的讀者一定知道,這一定會出現例外狀況。但是仔細想想,我們單純不管會不會出現例外,純粹只看函式被推論結果本身,該函數 probablyThrowsError 的型別應該被推論為 (number) => number,回傳型態之所以為 number 而已是因為 TypeScript 預設任何函式或方法回傳型態本身就包含 never 了。

而變數本身也是被註記為 number,接受的函式之回傳值 -- 其型別也是 number,所以對比型別之後照樣是通過的,TypeScript 當然不可能會對這個情況進行警示的動作(如圖八)。

https://ithelp.ithome.com.tw/upload/images/20190915/20120614fxZEqAP6rW.png
圖八:完全沒被 TypeScript 發出任何警告,很乾淨的樣子

好,我們回來看看這一句話:

函式或方法既有回傳值狀況下丟出例外,我們稱之為 never 取代了該回傳值型別的狀態,而變數在 TypeScript 裡也可以被指派這種 never 型別的情況

我們的程式碼範例中,probablyThrowsError(-5) 因為會在函式內部的判斷而選擇丟出例外,因此我們認為該函式回傳的是屬於 never 型態的狀況,而變數可以被指派這種是 never 狀態的結果

用一個更直白的方式來表達這種概念:

https://ithelp.ithome.com.tw/upload/images/20190915/201206141bXT6zaN9s.png

以上這段程式碼,讀者親自打上去的話 TypeScript 也會認定是正常的喔!而這個就是所謂的 never 型別事實上也是 assignable(可被指派到變數)的概念。

而所謂 never 可以被指派到任何型別註記之變數是指以下這種情況(檢測結果如圖九):

https://ithelp.ithome.com.tw/upload/images/20190915/201206147xRl9ONBL3.png

https://ithelp.ithome.com.tw/upload/images/20190915/20120614QFNFRBfwXr.png
圖九:就算函式必為 never 型別,然而變數之註記型別為 number 依舊還是被 TypeScript 認定正常,因為 number 型別跟 number | never 型別完全等效

根據我們本篇重點 2 所提到的原則,任何型別包含 never 本身可以被指派 never 型態的值,因此圖九之情況確實成立。讀者這裡還會很模糊的話,沒關係,看久了或有印象就好,但要記得這些機制,不然你可能會覺得 TypeScript 很莫名其妙,明明是會出錯為何都不幫我們警告一下,事實上原來是對於 never 的機制不理解所造成的錯誤啊。

我們再回頭來看看第二點:

第二點:No type is a subtype of or assignable to never (except never itself).

我們這一次因為在討論 assignable 在 never 的行為,因此我們簡化為:

No type is assignable to never(except never itself)

說白了,你今天註記某一個變數型別為 never,對於該變數來說,不是這個函式或方法的程序卡在那邊就是跟開發者說,這個變數所被指派到的程式碼狀態有可能會出現錯誤。

因此呢,如果你的變數註記為 never 卻可以被指派到值或者甚至是不回傳任意值但卻可以結束程序的函式或方法,那分明就是邏輯錯誤!(以下程式碼檢測結果如圖十,而錯誤訊息如圖十一)

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

https://ithelp.ithome.com.tw/upload/images/20190915/20120614OYPrUMfM03.png
圖十:這樣分明就是邏輯錯誤啊

https://ithelp.ithome.com.tw/upload/images/20190915/201206142bUNLSyrHG.png
圖十一:可以看到 number 型別不能被指派到 never 型別的變數裡

重點 3. never 型別與變數指派行為

  1. 若變數被註記為任意型別 T,變數除了可以被型別 T 的值指派外,也可以被指派屬於 never 型別的值
  2. 若變數被註記為 never 型別,則變數不能指派任意型別 T (除了 never 型別以外)的值

天阿!都已經講到這裡了,第三點豈不就很悲劇,又要再講很長一段。其實不會,根據以下的敘述:

第三點:In a function expression or arrow function with no return type annotation, if the function has no return statements, or only return statements with expressions of type never, and if the end point of the function is not reachable (as determined by control flow analysis), the inferred return type for the function is never.

其實第三點是在講函數型別中回傳值的型別推論機制,如果根據 Control-Flow Analysis(判斷式流程分析,學過編譯器或玩過語言設計的人肯定都聽過的東西)的結果,函式是不能被執行到結束,則該函式的回傳值會被自動推論成 never 型別。

其實我們一開始舉的無窮迴圈的例子以及丟出例外的例子就保證該函式絕對不會執行到結束,也因此才會被 TypeScript 推論為回傳型別為 never

讓我們來看看最後一點:

第四點:In a function with an explicit never return type annotation, all return statements (if any) must have expressions of type never and the end point of the function must not be reachable.

其實就是,一但將某個函式之回傳型態強行註記為 never必須對該函式開發到無論什麼狀況都不能讓函式的執行有結束的一天(亦或者是程式在這函式裡面一定會被中斷)。

因此以下的狀況一定會被 TypeScript 發出警告(錯誤訊息在圖十二):

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

https://ithelp.ithome.com.tw/upload/images/20190919/20120614YoNArszbsz.png
圖十二:TypeScript 很明確要求,函式必須要具備 Unreachable Endpoint(沒辦法結束執行)才能確定回傳值為 never 型態

重點 4. 函式型別回傳值的推論與註記

  1. 如果函式可以被完整的執行完畢,則 TypeScript 會根據 return 表達式回傳的值之型別或者是函式回不回傳值來作為根據進行型別推論
  2. 如果函式 100% 確定不能執行到結束的點,則 TypeScript 會無條件將該函式的回傳值之型別視為 never
  3. 如果函式被積極註記為 never 型別,則開發者必須確保該函式的實作 100% 不會有任何結束的執行點

小結

原本筆者還以為可以把 anynever 以及 unknown 一篇內強行結束,但實在是沒辦法。不過筆者在這裡也說得清清楚楚:never 型別的用意就是為了提醒開發者,有些程式碼之函式或者是方法的呼叫一定會使得程式卡住或者是中斷執行

至於為何要有這種機制呢?你可以想成我們刻意呼叫那些會拋出錯誤的函式,這樣一來,以後我們在開發時,我們一看到那些回傳型別為 never 的函式被使用,我們就可能得使用 try...catch... 的敘述式進行錯誤處理。另外,如果把 never 概念弄得很熟,對於理解後續的文章將會是關鍵的一大助力啊!


上一篇
Day 09. 前線維護・選用屬性 X 型別擴展 - Optional Properties
下一篇
Day 11. 前線維護・特殊型別 X 無法無天 - Any & Unknown Type
系列文
讓 TypeScript 成為你全端開發的 ACE!51

尚未有邦友留言

立即登入留言