[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
,所以會直接回傳a
DistributeUnion<'b'>
滿足any
,所以會直接回傳b
DistributeUnion<'c'>
滿足any
,所以會直接回傳c
最後就會等同於:
type DistributeUnionReturn = 'a' | 'b' | 'c';
這也就是為什麼,最終的回傳值會是 'a' | 'b' | 'c'
的緣故。
讓我們把它放在一起看: