[Day09] TS:什麼!型別也有分配律?理解 Extract 和 Exclude 的實作

上面這個是今天會提到的內容,如果你已經可以輕鬆看懂,歡迎直接左轉去看我同事的精彩文章 — 「From State Machine to XState」!
前幾天筆者提到的 Utility Types 多半是在 TypeScript 官方文件中提到的說明,但其實在 TypeScript 中也內建了一些 Utility Types,使用者不需要額外定義這些 Utility Types 就可以直接使用,這些內建的 Utility Types 列在官方網站的 references 中,今天就來讓看其中兩個內建的 Utility Types,分別是 Exclude 和 Extract。
Extract 和 Exclude 的基本用法
即使我們還不了解 Extract 和 Exclude 是怎麼被寫出來的,但可以直接使用它,就好像有時不了解某個功能是如何被實作出來的,還是可以直接呼叫它提供的方法或函式一樣。
還記得我們前面提到了解 Utility Types 的一個小技巧就是實際帶入一個型別,把它會回傳的內容存成一個 Type Alias 來看看嗎。讓我們先來看 Extract<Type, Union> 的使用:
// https://www.typescriptlang.org/docs/handbook/utility-types.html#extracttype-union
type T1 = Extract<'a' | 'b' | 'c', 'a'>; //  'a'
type T2 = Extract<'a' | 'b' | 'c', 'a' | 'b'>; // 'a' | 'b'
type T3 = Extract<string | number | (() => void), Function>; // () => void
type T4 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // 'a'
可以看到 Extract 需要接受兩個參數 Type 和 Union,但它會做的是把 Type 中滿足 Union 的取出,其餘不滿足的摒除掉,所以在:
- 第一個例子中,從型別 
'a' | 'b' | 'c'中留下滿足'a'的,所以最後得到a - 第二個例子中,從型別 
'a' | 'b' | 'c'中留下滿足'a' | 'b'的,所以最後得到a | b - 第三和第四個例子也是一樣的概念。
 
接著先來看 Exclude<Type, ExcludedUnion> 的使用:
// https://www.typescriptlang.org/docs/handbook/utility-types.html#excludetype-excludedunion
type T1 = Exclude<'a' | 'b' | 'c', 'a'>; //  'b' | 'c'
type T2 = Exclude<'a' | 'b' | 'c', 'a' | 'b'>; // 'c'
type T3 = Exclude<string | number | (() => void), Function>; // string | number
type T4 = Exclude<'a' | 'b' | 'c', 'a' | 'f'>; // 'b' | 'c
Exclude 的作用剛好和 Extract 相反,Exclude 雖然一樣需要提供兩個參數 Type 和 ExcludedUnion,但它會做的是把 Type 中滿足 ExcludedUnion 的剔除。所以在:
- 第一個例子中,從型別 
'a' | 'b' | 'c'中剔除a後,只會剩下'b' | 'c' - 第二個例子中,從型別 
'a' | 'b' | 'c'中剔除'a' | 'b'後,只會剩下'c' - 第三和第四個例子也是一樣的概念。
 
在知道了它們各種的用法後讓我們來看它們的實作。
Conditional Types 的分配律(Distributive Conditional Types)
讓我們先來看 Extract 的實作:

這裡用到了我們昨天提到的 Conditional Types 的觀念,讀者應該可以理解到原始碼的意思就是:
「如果 T 是 U 的子集 合,就回傳 T,否則回傳 never」
雖然我們已經理解了 Conditional Types,翻成白話文也完全正確,但在看到剛剛使用的範例是,卻好像覺得少了什麼,思路無法連貫:
type T1 = Extract<'a' | 'b' | 'c', 'a'>; //  'a'
不是說如果 'a' | 'b' | 'c'(T)滿足 'a'(U)的話,會直接回傳 'a' | 'b' | 'c'(T)嗎?為什麼最後只回傳了 'a' 呢?
這裡我們就要來提一下 Conditional Types 的分配律。「分配律」這個詞有一種熟悉但有離了很遙遠的感覺,但基本上我們一定都用過,例如:
a * (b + c) = a * b + a * c
上面這個就是乘法分配律。那麼什麼是 Conditional Types 的分配律呢?
假設說我們在 Utility Type 的泛型中帶入的不是單純一個型別,而是一個 union type 時會有分配律的情況產生。舉例來說,先定義一個用 Conditional Type 寫的 Utility Type:
// 定義一個用 Conditional Type 寫的 Utility Type
type DistributeUnion<T> = T extends any ? T : never;
接著在 T 的地方帶入 union type,像是這樣:
type DistributeUnionReturn = DistributeUnion<'a' | 'b' | 'c'>; // "a" | "b" | "c"
這麼寫的意思實際上等同於:
type DistributeUnionReturn =
  | DistributeUnion<'a'>
  | DistributeUnion<'b'>
  | DistributeUnion<'c'>;
也就是說原本的 'a' | 'b' | 'c' 會被分配到每個 DistributeUnion<T> 的 T 中在用聯集 | 起來,因為
DistributeUnion<'a'>滿足any,所以會直接回傳aDistributeUnion<'b'>滿足any,所以會直接回傳bDistributeUnion<'c'>滿足any,所以會直接回傳c
最後就會等同於:
type DistributeUnionReturn = 'a' | 'b' | 'c';
這也就是為什麼,最終的回傳值會是 'a' | 'b' | 'c'的緣故。
讓我們把它放在一起看:
