iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 8
0
Modern Web

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

Day 08. 前線維護・明文型別 X 格式為王 - Literal Types

https://ithelp.ithome.com.tw/upload/images/20190914/20120614LcWKFRyjnz.png

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

在什麼樣的情況下,你會怎麼決定要選擇使用 TypeScript 的陣列、元組(Tuple)或列舉(Enumerated)呢?

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

[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 喔~寫作過程當中會不斷更新的!

正文開始

明文型別 Literal Types

只要是型別有廣義物件或複合的格式,就是屬於明文型別的一種表現形式

讀者其實早就看過明文型別的長相,甚至 TypeScript 對於變數廣義物件推論的結果都是會以明文型別為主!什麼意思呢?還記得我們剛開始講到狹義物件的型別推論時,看到這個案例。(如圖一)

https://ithelp.ithome.com.tw/upload/images/20190914/20120614q7aAegoZlQ.png
圖一:這是我們在 Day 03. 提到的範例

其中,你可以看出 TS 推論出來的結果並不是 object,而是這個格式:

{
  name: string,
  age: number,
  hasPet: boolean
}

這個很明確地把物件格式印出來的結果,我們稱它為物件的明文型別(Object Literal Type)。

再者,我們再看下一個例子。(如圖二)

https://ithelp.ithome.com.tw/upload/images/20190914/20120614cnxMbMplos.png
圖二:這是我們在 Day 04. 提到的範例

同樣地,這個把函式的型別格式表達出來,我們可以稱此:

https://ithelp.ithome.com.tw/upload/images/20190917/20120614lkkSEMAB3r.png

為函式的明文型別,但我們也可以直接稱此為函式型別。(這是習慣的講法,所以其實我們在 Day 04 介紹的函式型別本身就是一種明文型別的表示方式喔)

理所當然,陣列、元組以及列舉本身各自成一種型別,但就廣義來說,都是明文型別的表示方式。英文單字:

literally (adverb.)
in a literal manner or sense; exactly. (明文上的感覺;實質上)

表達的意思很清楚:我們一看到什麼,長得就像是那副德性。(俗語說的:牛就是牛,牽到北京還是牛)

重點 1. 明文型別 Literal Types

只要是表達廣義物件的格式或者是任意型別(包含原始型別的)複合組合(unionintersection 也算在內)-- 就隸屬於明文型別的範疇

也可推得,通常 TypeScript 會將任何廣義物件的型別推論為明文型別的格式。

型別化名 Type Alias

有些本身有玩過一點 TypeScript 的讀者可能會想:“作者花了一個禮拜的時間,總算該講些好像應該要很早就講到的 TypeScript Feature!”

拖到現在才想講的原因不是因為懶得講或者是忘記講,而是因為型別化名通常會用在明文型別、複合型別(unionintersection)以及比較複雜的型別格式名稱必須作抽象化(Abstraction)等等。

那麼我們就正式定義型別化名以及用意到底如何:

重點 2. 型別化名 Type Alias

若我們有一個型別 T,T 可為任何的型別(包含原始型別、物件型別、TypeScript 內建型別、明文型別、複合型別、Generics 通用型別等)。其中我們想要讓該型別 T 等效於別名 A,則可以使用 TypeScript 的 type 關鍵字進行化名宣告:

type A = T;

型別化名的主要目的為簡化程式碼以及進行型別的抽象化(Type Abstraction)

貼心小提示

抽象化(Abstraction)的概念在程式的領域非常重要,如果讀者沒有聽過這個詞或者是覺得這概念很難理解,可以把它想成:如果今天我們要點餐廳的甜點,其中,每一道甜點一定會有相對應的食譜。但我們不會跟餐廳的人講說我們要點的甜點的食譜內容(也就是一長串做甜點的指令),而是直接稱呼甜點的名稱,這種直接稱呼這些代表性名詞(或化名)而不把食譜內容(程式碼實作細節)的作法即是抽象化過後的作用。

好的,通常我們使用型別化名的部分,一種是簡化程式碼,因此筆者這邊給大家一個範例:

https://ithelp.ithome.com.tw/upload/images/20190914/20120614mj9tOIoGom.png

以上的程式碼,主要是定義四種不同的數學運算形式(更精確來說是 Math Operator 數學運算子的概念),讀者從函式型別可以得知,這些函式型別推論結果為:

https://ithelp.ithome.com.tw/upload/images/20190914/20120614nhPtBLEP9e.png

好的,我們可能會覺得這一長串的明文型別想要代表我們正在定義數學的運算子(Operator),比如我們想要實踐數學次方的運算子,並且對其進行註記讓 TypeScript 幫助我們確認並防止錯誤:

https://ithelp.ithome.com.tw/upload/images/20190914/20120614YECsQa1rar.png

但這真的是太雜亂,而且複製來複製去的,又會違反 DRY(Don't Repeat Yourself)原則。因此我們可以建立個型別化名來簡化程式碼:

https://ithelp.ithome.com.tw/upload/images/20190914/20120614RDwB0E87AC.png

我們這一次再來玩一下,可能讓 TS 想要找你碴的狀況:

https://ithelp.ithome.com.tw/upload/images/20190914/20120614E2vqZaAz4C.png
圖一:很明顯地,型別化名真的幫助我們監控函式實踐的狀況

函式的明文型別之化名

還記得 Implicit any 是什麼樣的情形呢?(請看 Day 04.)即是當我們單純定義函式時,如果沒有任何型別註記,或者是運用 Generics 的狀態下,TypeScript 會對函式裡的參數直接推論為 any,而這個狀況會使得 TypeScript 會直接警告讀者說,這裡有潛在的 Bug 會對未來的使用上產生不穩定影響,亦或者是被誤用的可能性提高。

另外,還記得在 Day 06. 中,有提到函式的參數不需要積極註記的時機嗎?筆者原封不動貼給讀者看:

《陣列與函式 X 陣列與元組》之 重點 1. 函式的參數不需要被註記的情況

  1. 回呼函數在某些情況下不需要對輸入參數部分進行註記,原因是藉由通用型別 Generics 的機制,我們可以設計出讓 TypeScript 能夠藉由通用型別參數所獲取的外部型別資訊,提前預知到未來的程式碼執行的狀況下,對於各種變數、函式的輸入輸出、類別屬性與方法的型別等等 ... 的型別推論。
  2. 型別化名(Type Alias)的運用在大部分的狀況下也可以取代積極註記的必要性。

請看第二點:

型別化名(Type Alias)的運用在大部分的狀況下也可以取代積極註記的必要性

其實,讀者想一下,如果你是 TypeScript,當你讀到開發者跟你說:

https://ithelp.ithome.com.tw/upload/images/20190917/201206147oO0ADecQF.png

然後你又再後續的程式碼看到:

https://ithelp.ithome.com.tw/upload/images/20190917/201206141im7ov5lvi.png

表面上,這裡的函式中的參數 n1n2 都沒有被積極註記,看似會引發 Implicit any 這種容易被質疑的機制。但實際上,因為我們有跟 TypeScript 講說,我們的 powerOp 早被視為 MathOperator,也就是剛剛剛那一長串 (n1: number, n2: number) => number 這種明文型別表示方式。因此呢,TypeScript 就會很自動地把我們的函式裡的參數 n1n2 之型別推論為 number 並進行監控的狀態。

因此,以下這些行為會被 TypeScript 開罰單。(錯誤訊息如圖三)

https://ithelp.ithome.com.tw/upload/images/20190914/201206149uGiUeM4Sb.png

https://ithelp.ithome.com.tw/upload/images/20190914/201206146LAWQ8YNxY.png
圖三:我們將參數換成字串型態,照樣被 TypeScript 認為是錯誤!

因此呢,針對這個問題:“什麼時候不需要對函式的參數進行積極註記?”,這一點,我們又進一步補足了一個小坑,最後的大坑就是探討 Generics 的機制才會完美解決這個問題~

重點 3. 函式型別的化名

若我們對型別 T 進行型別化名 U,其中 T 為函式型別,亦即:

https://ithelp.ithome.com.tw/upload/images/20190917/20120614B2ERkOk0tu.png

其中,任何變數 A 被型別化名 U 註記,則該變數被指派到的函式值,不需要積極註記,因為早在定義化名的時候,這個步驟就被做掉了。

狹義物件的明文型別之化名

貼心小提示

可能有些讀者是跳著看文章,因此這裡再次進行筆者自創的名詞補充(這些自定義名詞詳細說明可以在 Day 03. 的文章看到)

  • 狹義物件:泛指 JSON 物件格式(即 {} 這一類 JS 物件)
  • 廣義物件:泛指 JSON 物件、函式、陣列、類別以及類別創建之物件等
  • 完整性定律:保持物件之完整性的成立條件在於
    • 不隨便新增該物件原本就沒有的屬性
    • 不指派錯誤型別的值覆寫到物件的屬性
    • 不指派錯誤物件格式的值覆寫整個物件

如果假設我們將本篇開頭第一個範例進行型別化名:

https://ithelp.ithome.com.tw/upload/images/20190914/20120614c2J3cob33i.png

除了可以簡化那冗長的狹義物件的明文格式外,光是從 PersonalInfo 抽象化的結果,開發者馬上就知道這段程式碼本身的意義就是在定義一個人的資訊的格式。

一如往常地,我們接下來要進行深入實驗探討 TypeScript 會幫我們管控的錯誤有哪些。(如圖四)

https://ithelp.ithome.com.tw/upload/images/20190914/20120614iXUQtV0J4n.png
圖四:跟我們預期的一樣

結果是,我們積極註記這種狹義物件的明文型別的化名,它還是依然符合 Day 03. 提及的狹義物件的完整性假說。(這應該是廢話,我們都跟 TS 講好這個東西長什麼樣子了)

讀者試試看

因此筆者驗證出來,某變數 A 進行狹義物件之明文型別的註記,該變數 A 就符合了狹義的物件完整性定律,也就是:

  • 只要代入的值其對應屬性的型別吻合則通過
  • 只要全面覆寫的狹義物件格式(包含屬性對應之型別)則也通過
  • 其餘破壞狹義物件完整性的行為:包含新增值、代入錯誤的型別值到物件的屬性裡以及覆寫錯誤的狹義物件格式

因此建議讀者可以試試看,將此現象推廣到廣義物件完整性定律,過程可以參考 Day 03.,只是換成這一次我們將步驟換成:

  • 將廣義物件進行型別化名
  • 將變數積極註記成該化名
  • 測試該變數存的值有沒有符合完整性定律

重點 4. 廣義物件型別的化名

符合廣義物件完整性定律(參照 Day 03.)並且結合化名帶給開發者的功用,即是整理程式碼並且進行抽象化的動作。

因此呢,陣列、函式等等的案例,筆者這裡就不多說了!

狹義物件的明文形式作為函式參數的狀況

“這篇還沒好啊!?” - 不僅是讀者,筆者其實也感到歇斯底里

這是最後一個本篇要提到的重點,但我們再稍微撐一下下。

首先,這裡要小心對待這兩個名詞:狹義物件的明文型別 V.S. 狹義物件的明文形式

偏偏都在講狹義物件(即 JSON 格式之物件),但差別就差在型別形式的不同,狹義物件的型別格式讀者早就知道:

type PersonInfo = {
  name: string,
  age: number,
  hasPet: boolean
};

但是形式呢?其實簡單來講就是(Value)啦:

{
  name: 'Maxwell',
  age: 20,
  hasPet: false,
}

(哎,筆者希望不要再自創詞了...不過中文差一字總是會讓人誤會。)

好了,既然已經澄清好這兩個名詞了,我們探討以下兩種狀況,讀者猜猜看,以下哪段程式碼會不會被 TypeScript 質疑?

https://ithelp.ithome.com.tw/upload/images/20190914/201206149Plb1AYZRh.png

被 TypeScript 檢查的結果如圖五。

https://ithelp.ithome.com.tw/upload/images/20190914/20120614B5sQOaYXuH.png
圖五:第一個案例,如果物件形式(值)直接被丟進函式就會被警告,但是第二種情況竟然不會

似乎呢,TS 針對那種俗稱 Object Literal -- 也就是物件明文的形式直接代入函式作為參數的狀況會嚴格進行型別的認證;但是,我們如果經過中間的變數存取並代入函式後,我們竟然發現 TypeScript 理都不理我們!(感到傷心了嗎)

因此呢,這裡可能是讀者在使用 TypeScript 時候,有很大機率會踩到的雷 -- 是超級需要避雷之區域,我們又更知道了積極註記的重要性!

如果當初我們對該變數進行積極註記時,在我們還沒代入變數到函式前,TypeScript 應該就會告訴我們,變數得到的值裡面多了 hello 以及 nothingSpecial 這些原本在 PersonInfo 沒有的屬性啊!

不過這裡筆者必須提到,參數對於(未被註記的)變數檢測,是只要變數符合到參數對應之型別的格式就算通過,如果有聽過一些程式上的技巧的話 -- 這算是 Duck-Typing 的模式,不過因為我們使用 TypeScript 就是希望它能夠幫助我們確保這些值不會有不可預測的變動(筆者認定:型別具備靜態的特性),因此筆者這裡實在不建議在 TS 使用 type 來結合 Duck-Typing 技巧

但使用 TypeScript Interface -- 也就是介面結合 Duck-typing 技巧,筆者認為可以喔!不過要請讀者等到 Day 13. 的文章啦~。 XD

這裡筆者在特別把這個狀況再敘述出來:

若某變數 A 儲存某物件值,其中 A 沒有被積極註記(因此 TypeScript 會對 A 作型別推論,推論出 A 的明文型別格式)。

另外,若變數 A 作為某函式(或方法)的參數,其中該參數有型別 T,則 TypeScript 只會針對 A 的格式至少符合型別 T(屬性型別對應正確、少一鍵不行但多一鍵以上都可以),則變數 A 通過該型別 T 的檢測。

不過呢,讀者可能會問,狹義物件(就是剛剛利用 JSON 物件舉的例子)既然都會這樣了,那廣義物件需不需要對變數進行變數上的註記行為?

基本上,筆者認為,只要是針對任何物件(不包含原始型別),我們應該選擇積極註記;儘管 TypeScript 在物件的推論上是成熟的,會把型別格式等等資訊告訴開發者,但因為有這種很模稜兩可的狀況 -- 也就是用變數推論的方式 Bypass 函式的參數型別的確認機制,使得我們不敢對 TypeScript 掉以輕心。

重點 5. 廣義物件的註記

由於我們可以藉由存取廣義物件的表現形式在某變數裡,其中該變數沒有被積極註記,儘管該物件的值不完全符合函式(或方法)的參數所註記之型別,但只要該變數至少有符合型別的格式,依然可以通過函式(或方法)參數對於變數的值的驗證。若想避免此狀況發生,任何變數需要存取廣義物件時,必須進行積極註記型別的動作

其中,至少有符合參數型別的條件有三,假設沒有被註記的變數為 A,而將其代入某函式(或方法)作為參數,其參數型別為 T:

  1. A 必須與 T 規定的必填屬性與型別對應完全正確
  2. 可以忽略 A 對 T 差集出來的屬性(相對 T 多出來的屬性)
  3. A 不能缺少任一個 T 所規定的屬性

[2019.09.18. PM 05:25 新增訊息]
儘管這個案例看起來很麻煩、很複雜,但是本系文章到筆者目前寫作進度(Day 17.),已經重複提及至少 3 次了~至於重不重要,讀者請自行評估~

這很明顯,一個不明狀況打壞了我們前幾天不停討論的 TypeScript 針對各種物件推論的機制情形,以為我們不需要對存取物件的變數進行註記。可我們真的就錯了!筆者也覺得學習認得這個模式也是麻煩,不過這就是要掌握工具必須經歷的克難期。

有沒有可能未來改掉這種狀況,個人覺得不可能。TS 都已經發展三版了,再加上原生的 JS 你不得不說~它真的很自由,因此要寫出好的程式碼,我們勢必要自律呢。

不過呢,聰明的讀者可能想說:

“恩 ... 可是這樣不是很方便嗎?函式的參數只要確定代入的物件形式擁有該有的屬性與正確的型別,這樣我們不就可以使用任何符合這個參數定義的物件格式的擴充形式作為該函式的參數?Duck-typing 本質應該就是這麼樣子,不是嗎?”

不過筆者必須再次重複強調,這個具有很彈性的擴充性照理來說是 TypeScript Interface(也就是介面)的專屬,不過很明顯 TypeScript 的型別系統 type 這樣搞只會導致更多讓人誤解的程式碼出現,這樣一來,乾脆不要用 TypeScript 開發還比較不容易犯錯啊。況且 type 的意義是:靜態的資料型別格式,怎麼能夠被我們動來動去呢?

讀者若真的好奇,請記住黑暗數字 -- 13 -- 本系列在 Day 13. 會再強調一次 Type 與 Interface 之間到底意義為何;而 Day 16. 則是會將 Type V.S. Interface 這個重大議題搬到檯面。

小結

總而言之,我們除了得知明文型別以及型別化名的運作機制外,至少還指出了 Type System 可能造成開發上的盲點。不過筆者也將會在後面的篇章談到 type 以及 interface 的差別。那今天就先這樣囉~


上一篇
Day 07. 前線維護・列舉型別 X 主觀列舉 - Enumerated Types
下一篇
Day 09. 前線維護・選用屬性 X 型別擴展 - Optional Properties
系列文
讓 TypeScript 成為你全端開發的 ACE!51

尚未有邦友留言

立即登入留言