前情提要
在 TypeScript 中,Omit 和 Pick 是廣受喜愛的 Utility Types,
它們允許你從現有型別中排除或選擇特定的屬性來創建新型別。
參考我之前的文章
本文
基礎示範:Omit 的使用
我們先來看個簡單的例子,假設我們有一個 Game 型別,其中包含 id、name 和 price 三個屬性。
1 | type Game = { |
Omit 可以幫助我們排除 id 屬性並創建新型別 GameWithoutIdentity
這在單一型別中運行良好,但當我們引入 Union Types 時,就有一些細節值得討論。
問題:Union Types 中的 Omit 行為
假設我們有三個型別:Game、VideoGame 和 PCGame。
它們的 id 和 name 屬性相同,但每個型別都有其獨特的屬性。
1 |
|
當我們將這三個型別聯合起來形成 GameProduct 並嘗試使用 Omit 排除 id 時,結果卻不是我們預期的。
1 | type GameProduct = Game | VideoGame | PCGame; |
你可能期望 GameProductWithoutId 是三個型別排除 id 屬性的 Union Type,但實際上,我們只得到了這樣的結構:
1 | type GameProduct = { |
這表示 Omit
在處理 Union Types
時,並沒有對每個聯合成員單獨操作,而是將它們合併成了一個結構。
原因分析
這種行為的根源在於,Omit
和 Pick
不是 Distributive
的 Utility Types
。
它們不會針對每個 Union Type
成員進行個別操作,而是將 Union Type
視為一個整體來操作。
因此,當我們排除 id 屬性時,它無法處理每個成員型別中的不同屬性。
這與其他工具型別如 Partial
和 Required
不同,這些工具型別可以正確地處理 Union Types,並在每個成員上應用。
1 | type PartialGameProduct = Partial<GameProduct>; |
解決方案:Distributive Omit 與 Distributive Pick
要解決這個問題,我們可以定義一個 Distributive 的 Omit,這個版本會針對 Union Type 的每個成員進行操作。
1 | type DistributiveOmit<T, K extends PropertyKey> = T extends any |
使用 DistributiveOmit 後,我們可以正確地得到想要的結果:
1 | type GameProductWithoutId = DistributiveOmit<GameProduct, "id">; |
這將生成以下結構:
1 | // 所以 |
現在,GameProductWithoutId 正確地成為了每個型別的 Union Type,並且成功地排除了 id 屬性。
Distributive Pick
類似的,我們也可以定義一個 Distributive 的 Pick:
1 | type DistributivePick<T, K extends keyof T> = T extends any |
這個 Distributive
版本的 Pick
確保了你所選擇的屬性實際存在於你正在操作的型別中,與內建的 Pick
行為一致。
總結
Omit
和 Pick
雖然在單一型別中表現良好,但在 Union Types
中,
它們並不是 Distributive
的,這可能導致意想不到的結果。
我們可以創建 DistributiveOmit
和 DistributivePick
,
使它們能夠針對每個 Union Type
成員單獨進行操作,從而獲得更為預期的結果。
如果你在專案中遇到這類問題,記得可以考慮使用自定義的 Distributive
版本來處理 Union Types
,這樣可以避免踩雷!
(fin)