iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 17
2
Modern Web

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

Day 17. 機動藍圖・複合型別 X 型別複合 - TypeScript Union & Intersection

https://ithelp.ithome.com.tw/upload/images/20190918/201206149vJRZizAqL.png

閱讀本篇文章前,今天什麼都不用想!

直接進入正文,快看下面!

筆者就直接讓油門繼續摧下去~正文開始

筆者 O.S.:今天又是數學時間,要學好程式可真不簡單,但學好數學可以應用在程式上,何嘗不可?

複合型別 Union & Intersection

數學意義的集合 V.S. 複合型別

基本上,筆者發現有些在學 TypeScript unionintersection 的過程,有人會以為,或者是有這樣的誤解

TypeScript unionintersection 跟數學上對於集合的聯集與交集的定義是一樣的

很抱歉,以上那句話 —— 大・錯・特・錯 —— 所以被筆者狠狠地劃上了很大的刪節線!

筆者有點想把取這個很讓人誤解的名詞的人給推入火坑

以下就是踢爆這個誤區的推論時間!

數學定義上的聯集(Union)與交集(Intersection)的概念,通常會有簡單的 Venn Diagram 來展示,我們有集合 A 與 B 呈現如圖一:

https://ithelp.ithome.com.tw/upload/images/20190918/2012061418y8AfaIew.png
圖一:集合 A 的範圍為左方橘色圈圈部分;集合 B 的範圍為右方藍色圈圈部分

A 聯集 B(又稱 A 與 B 的 Union)為 A 與 B 包含在一起的範圍 —— 其中,聯集過後元素不可重複,這是集合本身的特性。

A 交集 B(又稱 A 與 B 的 Intersection)為 A 與 B 重疊的範圍。

假設 A 集合包含以下元素:{ 1, 2, 3, 4 }
假設 B 集合包含以下元素:{ 2, 4, 6, 8 }

其中 A 聯集 B 的集合結果為:{ 1, 2, 3, 4, 6, 8 }
而 A 交集 B 的集合結果為:{ 2, 4 }

那以上的定義跟 TypeScript 的複合型別有什麼差別?

首先筆者先從最簡單的方向開始,也就是我們常看到的聯集 union

https://ithelp.ithome.com.tw/upload/images/20190918/20120614nVvxMLNKPs.png

如果按照數學的概念推理的話,A 為 numberstring 的聯集,也就是說 UnionSet1 可以是 numberstring。不過筆者再舉下個例子:

https://ithelp.ithome.com.tw/upload/images/20190918/20120614chPx85eLPo.png

讀者可能覺得:“作者是想表達什麼?這麼簡單的事情:UnionSet2 可以是 UserInfo1 或是 UserInfo2 啊,因為 UserInfo1UserInfo2 都是屬於 UnionSet2 的範疇。”

好,請注意這句話:

UnionSet2 可以是 UserInfo1 或是 UserInfo2

那根據數學推理,照理來說應該只會有這三種組合:

https://ithelp.ithome.com.tw/upload/images/20190918/20120614Tdc7FkUfRA.png

以上就留給讀者進行驗證,TypeScript 檢測結果不會出錯。

好的,那麼以下這些結果,根據強制性的數學推理:就算某變數被註記為 UnionSet2,而該變數裡的值已經完全滿足 UserInfo1UserInfo2 其中一種型別,若另一個型別若完全不滿足的話,理應來說 TypeScript 要發出錯誤警訊。(以下程式碼檢測結果如圖二)

https://ithelp.ithome.com.tw/upload/images/20190918/201206149oRfkYwem0.png

https://ithelp.ithome.com.tw/upload/images/20190918/20120614udBPFSdniM.png
圖二:理所當然,第一個案例一定錯,因為既不滿足 UserInfo1 也不滿足 UserInfo2;然而後續的例子,只要至少其中一個型別被判定滿足,不管其他型別有沒有完整補齊,TypeScript 認為無所謂

筆者舉一個更諷刺的例子:空集合。(Empty Set)

https://ithelp.ithome.com.tw/upload/images/20190918/20120614X7TtcUIC6j.png

讀者一看就知道這一定會出錯(讀者可以自行驗證),因為什麼都不滿足

然而,筆者必須請讀者回憶一下:“集合論裡,空集合不也是屬於任何聯集過後的集合中的元素嗎?” —— 這個機制就很像原生 JS 裡還是必須要有代表空值的 undefined 或代表這個概念的值的 null。(當然啦,總是會有開發者閒 nullundefined 這兩種東西比較起來還是很蠢,不過這裡筆者沒有什麼太大的異義)

藉由以上的試驗,TypeScript 的 union 型別完全不符合數學理論所預期的規則。綜觀 TypeScript 已經出現很久了,使用複合型別的過程中,開發者們在認為理所當然的應用情境下違反了以前學過的最基礎的數學原理,甚至也沒意識到這樣的嚴重性,造成錯誤的觀念亂散播出去。(一開始就對數學家的專業不尊重,遑論尊不尊重軟體開發者的專業

再來想一下另一種案例,數學的交集跟 TypeScript 的 intersection 有沒有差別。

然而筆者不得不說,這裡數學定義的交集又跟 TS 的 intersection 完全背道而馳!請讀者想想看,以下的案例:

https://ithelp.ithome.com.tw/upload/images/20190918/20120614KR0yEXhxh4.png

根據 UserInfo1UserInfo2 各自的型別格式,有沒有認真想過它們有何交集點

nameage 以及 hasPetownsMotorcycle 的組合 —— 兩組屬性的集合中,完全沒有交集!

偏偏在 TypeScript 裡 —— 交集 intersection 的用法是:將兩個可以為型別或介面的組合裡的格式進行結合的概念

也就是說,如果我們註記 IntersectionSet 在某變數 B 上,該變數 B 必須實踐出 nameagehasPetownsMotorcycle 這四種屬性!否則會出錯呢。(以下程式碼檢驗結果如圖三)

https://ithelp.ithome.com.tw/upload/images/20190918/201206148NqJCQFtGA.png

https://ithelp.ithome.com.tw/upload/images/20190927/20120614MBjE0oH2H5.png
圖三:屬性缺一不可啊!

在這裡,筆者必須向讀者澄清,這怎麼能說是交集呢?

筆者認為,我們應該換個想法 —— TypeScript 的 unionintersection 的原理沒有跟數學的集合論符合,但倒是符合另一個模型!讀者想得到嗎?

布林代數的邏輯(Boolean Logic)!

學了那麼久的 AndOr 邏輯,應該很明顯的:| 是我們常看到的 OR 的概念;& 則是我們常看到的 AND 的概念

想想看,剛剛使用 union 時的情境:

UserInfo1UserInfo2 進行 union -- 把它轉換成:將 UserInfo1UserInfo2 OR 起來之後是不是邏輯通順很多?

  • 你可以選擇只要符合 UserInfo1 要求的型別或介面格式
  • (OR)你可以選擇只要符合 UserInfo2 要求的型別或介面格式
  • 但你也可以全部都符合
  • **不過就是至少一個條件一定要滿足,否則出錯!**因此空集合概念在這裡也不覆存在,會被認定是錯的,因為 OR 邏輯本來就是建立在其中一方符合的條件下才能滿足的。

對比使用 intersection 時:

UserInfo1UserInfo2 進行 intersection -- 把它轉換成:將 UserInfo1UserInfo2 AND 起來之後是不是邏輯通順很多?

  • 你必須符合 UserInfo1 (AND)UserInfo2 的型別或介面格式
  • 只要少了一個屬性就會出錯,完全符合 AND 邏輯的真諦

當初取 unionintersection 這兩個名稱的研發者,不是數學概念不好,就是亂用取錯名,簡直是誤導群眾、誤導開發者。

重點 1. 複合型別的基礎法則 Fundamental Law of TS Intersection & Union

複合型別(intersectionunion)在 TypeScript 的運作邏輯完全不等於數學裡集合論的定義。相對地,複合型別的概念反而是跟布林邏輯的概念符合

重點 1. 都已經用『 法則 』這兩個字形容了,應該可以提醒讀者這個概念的重要性了吧。

重點 2. 複合型別的語法與規則

假設某型別化名 AB,其中 AB 各自可以為型別或者是介面,亦或者都是型別或介面,其中 TUnionABunion;而 TIntersectionABintersection,則:

type TUnion        = A | B;
type TIntersection = A & B;

任意變數 C,其中 C 被註記為 TUnion 型別,則 C 的值必須至少符合 AB 其中一項型別的完整靜態格式(或實踐出其中一個介面裡的所有功能,如果 AB 有存在介面宣告的話)。

任意變數 D,其中 D 被註記為 TIntersection 型別,則 D 的值必須完全符合 AB所有的型別靜態格式(或實踐出其中一個介面裡的所有功能,如果 AB 有存在介面宣告的話)。

接下來就要看一些使用 unionintersection 會發生的莫名有趣現象。

讀者可能以為 unionintersection 就只是剛剛講的就結束了~

沒有喔,還有很多可以講 XD,因此筆者才會放在很後面。

原始型別的複合 Primitive Types Union & Intersection

我們剛剛有展示過,原始型別的複合 -- 對 AB 型別使用 union,其中 A 不等於 BAB 皆屬於原始型別

我們都很熟悉 AB 進行 union,然而我們可曾想過將這 AB 型別作 intersection 的結果?

試想一下:number & string 到底是什麼?其實讀者如果有把本系列文章讀熟一定會有答案呢!

筆者突然浮現出的 Brainstorming 過程:

“恩 ...... 要能夠同時為 number 以及 string ... 難道不是空集合嗎?”

“可是空集合在 TypeScript 作者好像沒講到啊 ...”

“世界上真的有既是數字和字串的東西嗎?還是說將數字加兩個引號就可以了?就像這樣:'42'

筆者回答:當然不存在,但是隱身於所有型別當中的共通點 —— 以下這句話節選自 Day 10. Never 型別

never is a subtype of and assignable to every type.

哦~其實這也挺合理的:“既然是不存在的型別交集,最後的結論理應是:例外狀況不可能的狀況出現,因此推得兩個原始型別的交集結果是 never 型別”。(印證的結果如圖四)

https://ithelp.ithome.com.tw/upload/images/20190918/20120614bJyoGGAp0Q.png

https://ithelp.ithome.com.tw/upload/images/20190918/20120614kTopT3guv3.png
圖四:原始型別交集結果就是 never

讀者是不是覺得 Never 型別存在的意義 比想像中重要?筆者認為其他的教學資源基本上只是幾百字不到淺淺帶過 never 型別的語法與用途 —— 但從來沒有把 never 的真諦傳出去,實是可惜,不然這些特殊型別的行為也挺有趣的。

重點 3. 原始型別的交集

TATB 皆為原始型別,TATB 不相同,則兩型別被 intersection 的結果為 never 型別:

type MustBeNever = TA & TB;

因此,MustBeNevernever 型別

讀者試試看

如果將廣義物件型別跟原始型別進行 intersection

  1. 結果判定是 never 嗎?
  2. 如果不是的話,那效果上跟 never 差不多嗎?

不過讀者仔細想想,這不就跟 Never 型別那一篇 ,後面的 intersection 提到的概念很像嗎?

《 Day 10. 特殊型別 X 永無止盡 - Never Type 》之 重點 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

其實本篇的重點 3. 不過就是 Never 型別篇章 重點 2. 的擴充呢!

型別檢測 Type Guard

其實筆者真的覺得中文好難翻,甚至也懶得去查 —— 寫作到這裡偶然查到原來 Generics 的翻譯為泛用型別並不是通用型別。(筆者掩面感到丟臉)

因此,可能會將文章裡的使用詞再進行修改。不過 Type Guard 筆者也很難找到翻譯,只知道可以被形容成型別限縮的概念

後來覺得『 型別檢測 』這名詞好像也不錯,乾脆就採用這個名詞 —— 作用依然還是跟型別的限縮有關!

讀者如果看過本系列,這個技巧應該在 Any v.s. Unknown 型別篇章 遇過一次,那時候是在討論 —— 如果變數被註記或推論為 unknown 型別時,該變數基本上什麼事情都不能做,這裡原封不動貼上當時的重點:

《Day 11. 特殊型別 X 無法無天 - Any & Unknown Type 》之 重點 2. unknown 型別下的變數指派限制

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

其中第二點的最後一句話:

“根據程式的控制流程分析,其 unknown 型別的推論被限縮到特定的型別 U 致使可以被指派到其他符合型別 U 條件的變數”

關鍵字是當時筆者說的兩個點(不是人體上的兩個點,筆者的有些朋友會這樣亂想,但這是公眾場合XD):“控制流程分析”與“推論被限縮”。

這很明顯在說明 Type Guard 的重點 —— 藉由簡單的判斷敘述來限縮型別的技巧,因此當時候才會有這個範例:

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

不過筆者這裡不在多做以上程式碼範例的說明,認為需要再複習 unknown 型別的概念請參考 Day 11.

那筆者為何到現在才講 Type Guard?

通常碰到 union 過後的型別,多數狀況下我們必須主動使用 Type Guard 讓 TypeScript 編譯器不會哀哀叫。其實之前在 介面的函式超載篇章 裡的例子舉得不錯,我們重新來看:

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

當時 AddOperation 的介面長這樣:

https://ithelp.ithome.com.tw/upload/images/20190917/201206143asAVcJzgK.png

其中我們遇到的狀況是,要實踐 AddOperation 難免會遇到 union 的狀況,該函式的參數 p1p2 各自可為 stringnumber,因此裡面才會需要 if...else... 判斷式進行參數型別的判斷。

我們今天來看看另一種狀況。

https://ithelp.ithome.com.tw/upload/images/20190918/201206144rqk9P0dD5.png

以上這個 ISummation 介面是屬於純粹函式格式的介面,也引用了函式超載的概念。

這裡想要達到的效果是 -- 假設某函式 F 已經實踐了 ISummation 所訂立的功能規格,則:

https://ithelp.ithome.com.tw/upload/images/20190918/201206141MwJQbqDjy.png

其中,若讀者不熟悉 ...args 這種在函式參數裡面的行為(這個叫做匯聚操作子 Rest-Operator),請多參考社群們熱心教學的 ES6 系列文章 ~

那麼以下筆者就不客氣地丟出實踐結果。

https://ithelp.ithome.com.tw/upload/images/20190918/20120614tezJ0FiLZu.png

筆者這一次是第四次編譯本並且測試結果。讀者如果曉得流程,應該也會直接果斷下 tsc 然後再去用 node 執行編譯出來的 index.js。(結果如圖五)

https://ithelp.ithome.com.tw/upload/images/20190927/20120614ARdiOGoyTG.png
圖五:驗證結果為正確

貼心小提示

有些讀者認為,檢測陣列可以用 Array.isArray,這一點也是沒問題的!不過記得要在 tsconfig.json 裡進行微調:

{
  "compilerOptions": {
    /* 略... */
    "lib": ["dom", "es2015"],
    /* 略... */
  }
}

有關於編譯器設定將會在《戰線擴張》系列進行介紹,筆者已經確定那時候是 30 天以後囉~

這裡筆者想強調的重點是 —— 通常檢測原始型別都是為用 typeof 這個關鍵字。

`typeof value === ''

而通常廣義物件或類別(Class)建造出來的物件則是會用 instanceof 這個關鍵字:

someObject instanceof ObjectBelongingClass

最常遇到的應該是諸如此類的問題:如何寫限縮型別的判斷式。

重點 4. 限縮型別的技巧 - 型別檢測 Type Guard

  1. 若想要過濾出純原始型別的值的話,使用 typeof 操作子
  2. 若想要過濾出廣義物件型別的值的話,使用 instanceof 判斷操作子,並填上屬於該物件型別所屬的類別
  3. 其他方式,譬如 Array.isArray 可以檢測陣列

小結

今天總算了結複合型別,筆者實在是感到溫馨。(讀者看到篇幅大小感到煩躁

筆者接下來要進行的是 TypeScript Class 也就是類別部分的介紹啦~

筆者這邊再次強調:你不需要懂 ES6 Class,筆者幫你建立這方面的基礎,因為基本上學完 TypeScript Class 就等於你會了 ES6 Class 大部分的內容~

而類別的部分也是為了鋪陳本系列後續的重頭戲必備的知識呀!


上一篇
Day 16. 機動藍圖・介面與型別 X 混用與比較 - TypeScript Interface V.S. Type
下一篇
Day 18. 機動藍圖・類別宣告 X 藍圖設計 - TypeScript Class
系列文
讓 TypeScript 成為你全端開發的 ACE!51

1 則留言

0
zaq159881
iT邦新手 5 級 ‧ 2020-09-12 14:52:28

感謝大大分享XD
最近剛好看到union 以及 intersection的地方
想說怎麼跟我原先的認知不同

如果從語法上的 & 以及 | 來看
用邏輯布林去理解時的確清楚許多了~

對,基本上 union 跟 intersection 我認為是當初訂規則的人對數學有某種程度誤用,這個關係應該用布林邏輯表示會很適合唷

我要留言

立即登入留言