iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 9
1
Modern Web

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

Day 09. 前線維護・選用屬性 X 型別擴展 - Optional Properties

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

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

  1. 明文型別(Literal Type)是什麼?
  2. 如何使用型別化名(Type Alias)?使用化名的好處是什麼?
  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 喔~寫作過程當中會不斷更新的!

這一篇算是筆者覺得有點頭痛的文章。讀者覺得:“恩 ... 這系列的每一篇文長到令人頭痛吧 ...”

啊啊!確實確實~(感到心虛)

不過這一篇頭痛的點是在於:例外狀況筆者感到特多,沒有全部驗證過也不知道,越自由的條件,相對來講,要背負的風險(比如對於工具的無知性)可也就升高了呢。讀者們可以選擇爬過一遍 TypeScript 官方的文件,又或者選擇經驗解法,而筆者就是選擇後者。

因此呢,筆者只能盡量把語法與經驗記錄下來了...

正文開始

選用屬性解析 Optional Properties

脫離死板的型別格式第一步

好的,那麼接下來我們要看一些讓型別系統稍微脫離死板一點的機制,那就是選用屬性(Optional Properties)。為何會說型別系統讓人稍微感到死板呢?其實很簡單,還記得前一篇我們提到的明文型別(Literal Types),其中以狹義物件的明文型別為例:

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

另外,型別系統 type 的概念也是定義一個靜態的格式,因此不能隨隨便便輕易對註記過或被推論過型別的變數進行任何結構性改變。(單一屬性的值更改或者全面複寫都是可以的,參見 Day 03. 物件完整性理論

上面的 PersonInfo 型別,若某變數被積極註記過後,該變數必須完全符合的條件有以下:

  • 必須有 name 屬性,對應型別為 string
  • 必須有 age 屬性,對應型別為 number
  • 必須有 hasPet 屬性,對應型別為 boolean

這完全就大大限制我們對於型別定義的自由程度,因為我們不可能每一次都可以定剛剛好的物件屬性值,比如說:你所建造的某網站具有使用者帳戶系統,然後你的客戶想在此建立一個帳戶。其中,帳戶可能會有:

  • account:可能為 Email,因此對應型別為 string
  • password:(Hash 過後的)密碼,對應型別為 string
  • nickname:暱稱,對應型別為 string
  • birth:出生年月日,對應型別為 Date,亦或者是拆成:
    • year:年,對應型別為 number
    • month:月,對應型別為 number
    • day:日,對應型別為 number
  • gender:性別,對應型別為 enum,分為男性 Male、女性 Female 以及其他 Other(或是你可能有諸如第三性等欄位)
  • subscribed:訂閱電子報或你的網站的產品更新訊息等,對應型別為 boolean

然而,使用者最初註冊帳戶登入時,大多數狀況是不會填入全部的選項,很明顯就是指個資這一回事。譬如,以上面的帳戶的資料結構設計來看,使用者不太會在註冊帳戶過程中填入 nicknamebirthgender 等欄位。(不然使用者老早就跑囉)

於是如果我們真的很純地將此資料格式寫成:

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

那我們可就碰到很大的問題了,我們的 nicknamebirth 以及 gender 欄位可就變成不定時 Bug 爆發的雷點。因此呢,讀者可能會憑藉熟悉的原生 JS 規則想到:“啊!既然那些欄位可能為空的話,而且空是只還沒有被定義的狀態下,我們應該可以跟 undefined 這個值進行複合型別(也就是 union)的動作。亦或者是也可以跟 Nullable Types 進行 union 也可以的啦~”

好的,那麼我們按這個想法來試試看(程式碼被 TS 檢查的結果如圖一,錯誤訊息如圖二):

https://ithelp.ithome.com.tw/upload/images/20190914/201206143tUFgEb3HP.png

https://ithelp.ithome.com.tw/upload/images/20190914/20120614VICabflCyV.png
圖一:TypeScript 看你不順眼,毫不留情打你臉(有押韻喔~)

https://ithelp.ithome.com.tw/upload/images/20190914/20120614Iu3thsJZrH.png
圖二:TypeScript 簡直就是想把你給逼瘋,真是死板到爆!

其實呢,有些更敏銳的讀者早就猜到 TS 還是會認為少了 nicknamebirth 以及 gender 這三種屬性。如果你還記得在Day 03.的文章時,以下案例一定會出錯(以下的程式碼就不貼上檢測過後的結果圖了,之前示範過):

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

因為 identityknows 這兩個屬性分別都是接 null 以及 undefined 這兩種儘管是 Nullable Types,但也是原始型別的一種,不屬於 string 這種型別。

Day 03. 文章是理解整個物件的型別推論與註記的關鍵,因此筆者特意把這一長串文字變成連到那一篇的文章的連結,如果對於物件的推論與註記行為不熟的話,筆者建議可以再去看看喔~!

我們這一次改成這個範例(結果如圖三,錯誤訊息如圖四):

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

https://ithelp.ithome.com.tw/upload/images/20190914/20120614eNygVBfL4L.png
圖三:TypeScript 判斷的跟我們想像的完全不同啊

https://ithelp.ithome.com.tw/upload/images/20190914/20120614fa81qkmZpy.png
圖四:TypeScript 依舊認為,你還缺少 knowsidentity 兩種屬性

這裡我們就可以對 undefined 型別下一個簡單的重點。

重點 1. undefined 作為物件屬性的型別

若將 undefined 作為物件某些屬性的型別,儘管 undefined 在原生 JS 的意味就是可以放置該屬性為空值,甚至是不去定義的狀態。但在 TypeScript 的世界裡:undefined 這種原始型別代表必須存取名為 undefined 這種值,並不是完全省略定義它

為了解決這種太過於死板的限制(其實這也是 TypeScript 本身的機制,久了以後讀者也會自然覺得很正常的),我們今日的主角誕生:

選用屬性註記(Optional Property Annotation)-- 單純就是一個 ? 符號來裝飾我們的物件屬性。

使用選用屬性註記 <Prop>?

有些讀者覺得總算已經等到這個時刻,想請筆者趕快解析這種東西,好讓 TypeScript 使用起來感到更自由。

沒錯!這東西的確可以讓人在使用型別上感到非常的自由,開發起來如果知道某些技巧保證好上加好、錦上添花、如虎添翼、大家可以一起賺大錢發大財!(記得:筆者有時 P 話一大堆,讀者要培養見怪不怪的好習慣)

根據之前我們想要設計帳戶系統的例子,我再把程式碼原封不動地呈現給大家看:

https://ithelp.ithome.com.tw/upload/images/20190914/201206143tUFgEb3HP.png

這種狀況會出錯已經是讀者知道的事實了,因此我們這次改寫成這樣(以下程式碼被 TypeScript 掃描結果如圖六,其中變數 accountMaxwell 之推論結果如圖七):

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

https://ithelp.ithome.com.tw/upload/images/20190914/20120614h6GIZnFGGl.png
圖六:哎呀!PASS了!通過 TypeScript 的稽查了!

https://ithelp.ithome.com.tw/upload/images/20190914/20120614KXfbS5k3r3.png
圖七:變數的推論結果當然就是 AccountInfo

哇,這一次我們總算可以不用去把所有的屬性叫出來也可以通過 TypeScript 的檢測,真是可喜可賀。

但是筆者這邊要另外強調,所謂知道某些技巧使得開發 TypeScript 過程越來越順利的秘訣,其實根本不難 -- 只要把鼠標從變數移到變數被註記的那個位置,也就是 AccountInfo 那串字上。(如圖八)

https://ithelp.ithome.com.tw/upload/images/20190914/201206142Mw77mZ40l.png
圖八:指到型別化名上會顯示 -- 該型別化名對應的詳細內容說明喔

這個小步驟之所以非常重要的原因是 -- 從圖八可以看出,我們藉由檢視該型別的化名得知,我們還有三個屬性分別為 nickname?birth? 以及 gender? 部分沒有被包含在物件的屬性裡。

細心的讀者甚至看到,TypeScript 還自動對那些被選用屬性註記的符號(也就是 ?)註記到的屬性 -- 對應的型別還會跟 undefinedunion。也就是說,你也可以選擇不忽略該屬性但是填入 undefined 這個值喔

而另外一個原因是:通常我們運用型別系統的化名,我們可能會把化名定義在某處,然而該化名可能會用在程式碼甚至是被其他外部的檔案引用

儘管型別化名有它的好處:簡化程式碼並且進行抽象化(Abstraction)的動作(如果不知道這兩個特性請看前一篇文章),但是工程師開發過程不可能看到某型別就一直在整個專案的檔案翻來覆去,就為了找到這個型別化名到底背後代表的型別格式結構長什麼樣子!這實在是太傷時間了,因此記得,我們可以藉由滑鼠滑動到型別化名上,得知其化名背後代表的型別結構喔

貼心小提示

藉由滑鼠滑動到型別化名上,得知其化名背後代表的型別結構:這個 Feature 是 VSCode 在 TypeScript 專案上有的 Feature,因此筆者不太確定其他的 IDE 是否有相同的行為機制。早在本系列的開頭,筆者就已經強調過為何使用 VSCode 會比較適合使用 TypeScript 開發相關的專案的原因囉。(同間廠商出品,保證驚喜連連)

請讀者注意:如果你是使用其他的 IDE 而沒辦法靠這樣的方式看到型別化名的結構,你可能要進行進階設定 -- 抑或者是,換個 IDE 吧~ 呵呵呵呵呵~(一點都不貼心的貼心小提示)

重點 2. 物件屬性上的選用註記

若某屬性 P 屬於某物件的明文型別 的屬性之一,且該屬性對應的型別值為 T,而 A 是該明文型別的別名,則:

type A = {
  P?: T
};

代表的意義是,被型別化名 A 作型別註記的變數,可以:

  1. 選擇性地忽略 P 這個屬性。
  2. 因為推論出來的結果會是以下的形式,因此也可以選擇寫出 P 屬性但填入 undefined 這個值:
{ P?: T | undefined }

讀者試試看

還記得在 Day 05. 的文章中,討論 TypeScript 針對陣列的型別推論嗎?其中一個範例筆者這裡就原封不動地貼上:
https://ithelp.ithome.com.tw/upload/images/20190914/20120614Mzb6dpaMZ6.png

而上面的程式碼範例被推論出來的結果。(如圖五)
https://ithelp.ithome.com.tw/upload/images/20190914/20120614JnFUNHpMAu.png
圖五:亂糟糟的推論結果

讀者有沒有從剛剛的結論中,隱隱約約地知道隱藏在裡面的機制是什麼呢?

檔案中快速查找型別化名被宣告的位置

有些時候我們還是會看不到型別化名背後的結構,這時候該怎麼辦?我要怎麼快速找到該化名連結到實際上在程式碼間的定義位置呢?

以上的問題到底是什麼意思 -- 這裡筆者快速舉另一個例子:

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

其中,這應該是讀者第一次見到複合型別中:intersection(而非 union)的用法,也就是 & 符號。我們將原本的 AccountInfo 拆成 AccountSystemAccountPersonalInfo 並且利用 intersection 將這兩個型別組裝成 PersonalAccount。好處是,我們可以將複雜的型別格式拆卸成小單元,而這些小單元可以被重複性地使用

好的,再來我們看一下,那個 PersonalAccount 化名被滑鼠滑到顯示的資訊為何。(如圖九)

https://ithelp.ithome.com.tw/upload/images/20190914/20120614EmdyEFbpCi.png
圖九:PersonalAccount 被 TypeScript 識別之結果

看起來很正常,對不對?但這裡筆者就提出一個關鍵問題:

“我知道 PersonalAccount 是由 AccountSystem 以及 AccountPersonalInfo 組成,但是我要怎麼知道 AccountPersonalInfo 這個化名背後存在的型別結構到底是啥呢?”

我們完全沒辦法光靠剛剛講的技巧得知這種案例 -- 型別化名又是由其他型別化名組成。因此筆者這邊必須告訴讀者:如何確切找到定義該化名的程式碼位置

很簡單,善用 VSCode 快捷鍵,按照原先的方法一樣,鼠標指到型別化名的位置,並且:

  • 如果使用 Mac,則按下 Command(⌘)
  • 如果使用 Windows,則按下 Ctrl

然後你就應該會看到,該型別化名原本底下沒有實線,這時候就出現了!(如圖十,請注意到該化名底下有實線)

https://ithelp.ithome.com.tw/upload/images/20190914/20120614JREB8qsSzb.png
圖十:型別化名底下出現實線

出現底下的實現時,點下去,編輯器的 Cursor 就會被自動導到該化名被定義的位置。(如圖十一,注意那個垂直白線的 Cursor 所在的位置)

https://ithelp.ithome.com.tw/upload/images/20190914/20120614p0JPUugf5S.png
圖十一:Cursor 焦點自動被放在型別化名被定義到的位置

因此,你可以再重複一次剛剛的步驟,檢視 AccountPersonalInfo 或者是 AccountSystem 這兩個型別化名背後的型別格式是什麼。是不是很方便?以後講到若將 TypeScript 檔案拆成不同的模組,或是這種跨越檔案甚至不同的套件時的情境,會再重複解說一次這個技巧,讀者如果不小心忘記也可以放心。(不過建議還是記一下XD)

重點 3. 快速查找型別化名宣告之位置

使用 VSCode 時:

  1. 將鼠標移動至你想要查找的型別化名上
  2. 如果使用 Mac,則按下 Command(⌘);如果使用 Windows,則按下 Ctrl
  3. 看到型別化名底下有實線提示,馬上點進去

遇到化名的宣告是由其他化名組成的形式,則不停重複以上的步驟下去直至找到你想要看到的資訊。

小結

我們終於把選用屬性(Optional Properties)至少該涵蓋的內容都講到了。而且呢,這邊的重點不僅僅只是要會使用它的語法以及運作機制,筆者這邊還特地強調開發時,尋找型別化名被定義到的地方,因為開發時免不了還是要查套件定義的化名背後的詳細內容。

這技巧不只可以用在內部開發的 TypeScript 相關專案上,你如果想試著去開源社群(如 GitHub)貢獻 TypeScript 相關的程式碼,想要去瀏覽那些開源專案的基本結構就可以從這小小的技巧 -- 省下大大的時間在一堆檔案裡翻來翻去,不用再去煩惱到底那些型別化名被那些超強大開源者放到哪裡去。

我們可以也對專案進行進階的架構,並且可以把我們的型別化名定義等 -- 順便寫些 Documentation,豈不一舉兩得!到後面講到跟 TypeScript Definition File 以及和熱門第三方套件協作時,我們就可以看得出這個技巧的重要性

讀者試試看

其實呢,我們除了有選用屬性外(Optional Properties),針對廣義物件諸如:

  • 函式也有選用參數(Optional Parameters)
  • 元組有選用元素(Optional Elements)

但把這兩個東西加入這篇文章,筆者也覺得太大了,而且重要性個人認為反倒是還好,因此放在這邊讓讀者自行嘗試看看這些 Feature 囉~

有空實驗一下,看看各種組合情況會有什麼結果!
https://ithelp.ithome.com.tw/upload/images/20190914/20120614JrrDl5iFIS.png


上一篇
Day 08. 前線維護・明文型別 X 格式為王 - Literal Types
下一篇
Day 10. 前線維護・特殊型別 X 永無止盡 - Never Type
系列文
讓 TypeScript 成為你全端開發的 ACE!51

尚未有邦友留言

立即登入留言