iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 5
1
Modern Web

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

Day 05. 前線維護・陣列型別 X 型別陣列 - Array Types

https://ithelp.ithome.com.tw/upload/images/20190912/20120614Dn84nOYazy.png

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

  1. 定義一個函式,最需要注意的點有哪些?什麼情形必須積極作型別註記呢?
  2. 函式的輸出部分通常(但不是全部)可以不用作型別註記的原因為何?
  3. 如果我們遇到一個函數輸出的型態為 any,需要注意什麼事情呢?

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

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

下一個比較常見的另一種廣義物件就是:陣列(Array),廢話不多說...

正文開始!

貼心小提醒

不清楚廣義和狹義物件的讀者,再次提醒最後一次,這是為了好講解這一系列而定義出來的詞彙,分別代表:

  • 狹義物件:JSON 格式物件
  • 廣義物件:JSON 格式物件、陣列、函式、類別與藉由類別創建出來的物件...等等。(也可以想成所有型別的集合對原始型別進行差集的動作)

這兩個詞的定義在 Day 03 有講到,如果覺得模糊可以回去看看喔。

物件型別之陣列型別解析

環境沿用

不多說明,一樣都是在 01-basic 資料夾裡的 index.ts 寫程式碼,有問題請看 Day 02 的設定。

陣列型別 Array Types

我們照樣從最基本的地方開始,實驗一下這段程式碼。(圖一和圖二為以下兩段變數宣告時,各自的推論結果)

// 全部都是數字
let numbers = [1, 2, 3, 4, 5];

// 全部都是字串
let strings = ['hi', 'how are you', 'goodbye'];

https://ithelp.ithome.com.tw/upload/images/20190912/20120614ga08T2Poyp.png
圖一:型別推論為 number[]

https://ithelp.ithome.com.tw/upload/images/20190912/201206140CDJEclYjU.png
圖二:型別推論為 string[]

簡單吧!TypeScript 會按照開發者認為的邏輯,把這種元素(Element)皆為同型別 T 的陣列T[] 的方式表示。

如果有出現以下行為,TypeScript 就會好好關心我們(結果如圖三):

https://ithelp.ithome.com.tw/upload/images/20190914/2012061421C27jXvpT.png

https://ithelp.ithome.com.tw/upload/images/20190912/20120614tbCAzzOeXd.png
圖三:TypeScript 在推論部分真的挺聰明地!

貼心小提示

這裡筆者做一些小補充:

  1. TypeScript 怎麼知道如果是 number[] 型別的陣列,呼叫了 push 方法就會確認是否填入正確型別的值呢?(在 number[] 這個範例裡,TS 確保 push 方法的參數帶入的型別必須是 number 而非其他型別)
  2. 既然 push 以及 concat 都可以隨機應變了,那底下運作的機制是什麼呢?
  3. 我們有沒有辦法實做出這種很聰明又很泛用的類別呢?

這些問題的答案將會在很遙遠的天數介紹名為 Generics 泛用型別來揭曉囉!(筆者是後來查到原來 Generics 的翻譯是“泛用”)

那我們來為難一下 TypeScript(以下程式碼結果如圖四)。

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

https://ithelp.ithome.com.tw/upload/images/20190912/20120614ebV8hHjtjZ.png
圖四:TS 針對混合型態的陣列推論結果為 (string | number)[]

結果 TS 對於這一類型的陣列,進行型別推論時就用 union 來解決。看起來,union 的概念幾乎每個章節都出現了,但 union 其實還有更麻煩的東西要講 XD,所以被放在很後面(Day 17.)。

因此,讀者暫時先記得 union 的作用類似於對各種型別進行 OR 結合的概念,姑且先給一個小提示。

union 小提示

let A: T | U | V;

A 為某變數,TU 以及 V 為若干型別註記。若將 TUV union 起來並註記在變數 A 身上,則我們稱變數 A 可以存取型別為 T 或是 U 或是 V 之值以及型別的複合型態(如:TU 皆有、或 TUV 皆複合再一起)

不過有關於複合型別更詳細的內容會在本系列後面一點的篇章講到。

英文小補充

通常稱某陣列儲存之元素之型態集合只有一種的話(比如:全部都只存數字),除了稱它為 Typed Array 之外,還有一種更精確的名稱為 Homogeneous Type Array,中文名稱為同質性陣列,聽起來很炫炮吧!

如果英文能力強或者是打程式軟體經驗本身就很豐富的讀者,一聽到 Homogeneous 可能就會想出它反義詞彙:Heterogenous。如果某陣列儲存之元素,將每個元素的型別集合起來超出一種以上(也就是可能某陣列同時存在數字或字串甚至還有可能有其他型別的話),除了只能稱它為 Array 的話,更精確的說法是 Heterogenous Type Array,中文名稱就叫做異質性陣列,聽起來比剛剛的更炫砲吧!

當我們把陣列的型別推論解析完畢時,筆者還是得展示一下以下的案例到底會發生什麼事情(沒辦法,要善用型別系統的優勢前,必須確認不會踩到雷,寧可提前知道 TS 本身的雷也不要因為不理解 TS 而被工具搞瘋):

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

至少 TS 推論出的結果 —— 似乎合情合理,自從我們用了剛剛的範例,除了型別不一樣外,物件格式不ㄧ樣也就用 union 串在一起了。(結果圖五~圖七)

https://ithelp.ithome.com.tw/upload/images/20190912/20120614B6nHM2CTmC.png
圖五:推論出來的結果是 ({ message: string })[] 型別

https://ithelp.ithome.com.tw/upload/images/20190912/20120614seT49UpUBk.png
圖六:推論出來的結果是 ({ message: string, revolt?: undefined } | { message: string, revolt: boolean })[] 型別

https://ithelp.ithome.com.tw/upload/images/20190912/20120614ZMqnw2wcyT.png
圖七:推論出來的結果是 ({ message: string } | { message: number })[]

哎呀,不過我們看一下中間的範例被推論出來的結果真的很怪異:

{
  message: string,
  revolt?: undefined
} | {
  message: string,
  revolt: boolean
}

這種型態其實可以被簡化成:

{
  message: string,
  revolt?: boolean
}

至於為何呢,讓筆者留到後續文章再說明(汗水直流),畢竟我們不能偏題啊,今天的目標是要好好把陣列的推論跟註記釐清。

至少到這裡,我們大略知道陣列的型別推論規則:

重點 1. 陣列的型別推論

若集合 S 為陣列裡所有元素各種型別的集合大部分的情形下,該陣列被 TypeScript 型別推論的結果是:

(集合 S 裡所有型別 `union` 的結果)[]

可以舉個簡單的例子來給讀者欣賞一下這其中之奧妙,連函式型別的集合都可以舉例(結果如圖八):

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

https://ithelp.ithome.com.tw/upload/images/20190912/20120614wTOUOod2PW.png
圖八:哇,漂亮!被推論出 ((number, number) => number)[] 型別

套入剛剛的重點,我們的集合 S 裡面就只有一種型別 —— (number, number) => number(參見Day 04. 函式型別

再舉另一種例子(結果如圖九):

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

https://ithelp.ithome.com.tw/upload/images/20190912/20120614WerbBjxgTY.png
圖九:推論結果為 (number[] | string[] | boolean[])[]

這裡可以看得出來,這個 arraysArray 擁有不同種類的元素型別。根據剛剛所講的重點,我們的 S 型別的集合這時就會包含三種不同的型別 —— 分別為 number[]string[]boolean[],所以我們可以把這些套入這個公式:(集合 S 裡所有型別union的結果)[],得出這樣的結論:

S = { number[], string[], boolean[] }
S 裡所有型別的 union = (
  number[] |
  string[] |
  boolean[]
)

套入公式得到:(
  number[] |
  string[] |
  boolean[]
)[]

恩,不過讀者應該會知道筆者會想做什麼。來出個陷阱題吧,不過這部分就留給讀者自行研究看看,真的有問題可以再討論!

讀者試試看

請問以下這段程式碼:變數 miscellaneousArraysArray 被 TypeScript 型別推論出來的結果為何?你認為有沒有違反筆者提出的重點 1 的公式呢?

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

推論公式隱藏的瑕疵

不過最後筆者還是補充一個東西,還記得筆者故意將重點 1 的公式又寫了某些(很機車的)字眼:“大部分的情形下”,那什麼叫做不符合重點 1 公式期待的例子呢?

其實你早就在前面的某處遇到了:

let objectsArray2 = [
  { message: 'Hello' },
  { message: 'Hi', revolt: true },
  { message: 'Goodbye' }
];

推論出來的結果為:

{
  message: string,
  revolt?: undefined
} | {
  message: string,
  revolt: boolean
}

看似符合筆者提出之公式,實質上筆者說了,這個型別就好像跟這樣子很像:

{
  message: string,
  revolt?: boolean
}

想要知道為何可以這樣簡化,理解明文型別的機制(參見 Day 08.)之後其實一點也不難。

不過呢,如果測試以下的狀況,這樣要討論起來就更複雜了(結果為圖十):

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

https://ithelp.ithome.com.tw/upload/images/20190912/20120614jamukyBqFp.png
圖十:推論結果又變成 ({ message: string, revolt?: boolean | undefined })[]

這裡的關鍵點跟選用屬性(Optional Properties)的機制有關,後續讀者看到明文型別的文章就會更清楚圖十出現的推論狀況。

陣列的註記

經由剛剛一連串對陣列推論的剖析,筆者覺得 TypeScript 對於型別的推論並不會出現太大的問題(前提是,我們正常使用這個型別系統)。但有一種情形是 —— 我們可能會初始化一個空陣列,這時你猜猜看會被推論成啥呢?(結果在圖十一)

let emptyArray = [];

https://ithelp.ithome.com.tw/upload/images/20190912/201206145dZJb3iSpn.png
圖十一:空陣列就會遇到傳說中讓人討厭的 any 型別,只是變成陣列型別的形式罷了 any[]

有些讀者可能早就猜到,是 any[]。因此,對於空陣列這個情形,我們要積極對該變數作型別註記

還有另一種情況你會想要對陣列做型別註記,那就是 —— 如果目前陣列的型別中,沒有出現你需要的型別值,你必須得對該陣列積極作型別備註。

什麼意思呢?

假設我們想要某陣列同時存在數字或 Nullable Types 作為空值:

https://ithelp.ithome.com.tw/upload/images/20190915/201206148VUfxGo2Tb.png

根據上面的程式碼,型別推論的結果一定是 number[]。而讀者應該會知道,因為裡面沒有包含 Nullable Type 的值,所以以下的程式碼一定會出錯(結果如圖十二):

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

https://ithelp.ithome.com.tw/upload/images/20190912/20120614SQdhi193wq.png
圖十二:TS 覺得 null 這種東西不屬於 number 類別,想當然不給你通過

為了祭出解決方案,於是要派上 union,明確讓 TypeScript 知道我們的陣列想要同時擁有數字跟 Nullable Type 型別(結果如圖十三):

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

https://ithelp.ithome.com.tw/upload/images/20190912/20120614cSxS3Y0S8e.png
圖十三:我們可以把 null 加進去我們的陣列嚕~

重點 2. 陣列的型別推論與註記時機

  1. 大部分的狀態下,陣列型別的推論是符合開發者期待的
  2. 除非遇到以下狀況,才需要對儲存陣列型別的變數積極地作型別註記:
    • 空陣列值必須積極註記,這是會了要革除 any 可能帶來的禍害
    • 陣列裡的元素沒有你要求的型別,可以用 union 技巧作積極的型別註記
  3. 為了程式碼的可讀性,通常一個陣列擁有多個型別的話(也就是 Heterogenous Type Array),建議還是用 union 註記一下,不然要在陣列裡面用人眼遍歷過陣列的每一個值對應的每個型別 —— 跟直接註記比起來:型別註記是比較恰當的選擇喔

小結

本篇大致上把陣列型別的推論與註記都說明得差不多了,不過呢,其實還有些小細節筆者還沒講到。由於本篇文章筆者認為已經夠大了,因此會把剩下還沒講到的重點,放置在明天哦!


上一篇
Day 04. 前線維護・函式型別 X 積極註記 - Function Types
下一篇
Day 06. 前線維護・陣列與函式 X 陣列與元組 - Array & Functions & Tuples
系列文
讓 TypeScript 成為你全端開發的 ACE!51

尚未有邦友留言

立即登入留言