網站搜尋

TypeScript - 將聯合類型轉換為交集類型


有沒有辦法將聯合類型轉換為交叉類型:

type FunctionUnion = (() => void) | ((p: string) => void)
type FunctionIntersection = (() => void) & ((p: string) => void)

我想對 FunctionUnion 應用一個轉換以獲得 FunctionIntersection

答案#1

你想把聯集轉成交集嗎?分配條件類型和從條件類型的推斷可以做到。 (不過我認為把交集轉成聯集是不可能的,抱歉)這裡有邪惡的魔法:

type UnionToIntersection<U> = 
  (U extends any ? (x: U)=>void : never) extends ((x: infer I)=>void) ? I : never

這會分發聯合 U 並將其重新打包成一個新的聯合,其中所有成員都位於逆變位置。這允許該類型被推斷為交集 I,如手冊中所述:

同樣地,對於反變位置中的同一類型變數有多個候選項會導致推斷出交叉類型。


讓我們看看它是否有效。

首先讓我將你的 FunctionUnion 和 FunctionIntersection 加上括號,因為 TypeScript 似乎會將聯合/交集綁定得比函數返回更緊:

type FunctionUnion = (() => void) | ((p: string) => void);
type FunctionIntersection = (() => void) & ((p: string) => void);

測試:

type SynthesizedFunctionIntersection = UnionToIntersection<FunctionUnion>
// inspects as 
// type SynthesizedFunctionIntersection = (() => void) & ((p: string) => void)

看起來不錯!

請注意,一般來說,UnionToIntersection<> 會暴露 TypeScript 認為實際聯集的一些細節。例如,boolean 內部顯然被表示為 true | false,所以

type Weird = UnionToIntersection<string | number | boolean>

變成

type Weird = string & number & true & false

在 TS3.6+ 中會被積極縮減為

type Weird = never

因為不可能有一個值同時是 string、number、true 和 false。

答案#2

當你想要多種類型的交集時,也存在一個非常相關的問題,但不一定要將聯合類型轉換為交集類型。沒有辦法直接得到交集,除非訴諸於臨時的聯合類型!

問題是,我們想要取得交集的類型 可能內部有聯集,這些也會被轉換為交集。守衛來解決這個問題:

// union to intersection converter by @jcalz
// Intersect<{ a: 1 } | { b: 2 }> = { a: 1 } & { b: 2 }
type Intersect<T> = (T extends any ? ((x: T) => 0) : never) extends ((x: infer R) => 0) ? R : never

// get keys of tuple
// TupleKeys<[string, string, string]> = 0 | 1 | 2
type TupleKeys<T extends any[]> = Exclude<keyof T, keyof []>

// apply { foo: ... } to every type in tuple
// Foo<[1, 2]> = { 0: { foo: 1 }, 1: { foo: 2 } }
type Foo<T extends any[]> = {
    [K in TupleKeys<T>]: {foo: T[K]}
}

// get union of field types of an object (another answer by @jcalz again, I guess)
// Values<{ a: string, b: number }> = string | number
type Values<T> = T[keyof T]

// TS won't believe the result will always have a field "foo"
// so we have to check for it with a conditional first
type Unfoo<T> = T extends { foo: any } ? T["foo"] : never

// combine three helpers to get an intersection of all the item types
type IntersectItems<T extends any[]> = Unfoo<Intersect<Values<Foo<T>>>>

type Test = [
    { a: 1 } | { b: 2 },
    { c: 3 },
]

// this is what we wanted
type X = IntersectItems<Test> // { a: 1, c: 3 } | { b: 2, c: 3 }

// this is not what we wanted
type Y = Intersect<Test[number]> // { a: 1, b: 2, c: 3 }

在給定的例子中的執行過程如下

IntersectItems<[{ a: 1 } | { b: 2 }, { c: 3 }]> =
Unfoo<Intersect<Values<Foo<[{ a: 1 } | { b: 2 }, { c: 3 }]>>>> =
Unfoo<Intersect<Values<{0: { foo: { a: 1 } | { b: 2 } }, 1: { foo: { c: 3 } }}>>> =
Unfoo<Intersect<{ foo: { a: 1 } | { b: 2 } } | { foo: { c: 3 } }>> =
Unfoo<(({ foo: { a: 1 } | { b: 2 } } | { foo: { c: 3 } }) extends any ? ((x: T) => 0) : never) extends ((x: infer R) => 0) ? R : never> =
Unfoo<(({ foo: { a: 1 } | { b: 2 } } extends any ? ((x: T) => 0) : never) | ({ foo: { c: 3 } } extends any ? ((x: T) => 0) : never)) extends ((x: infer R) => 0) ? R : never> =
Unfoo<(((x: { foo: { a: 1 } | { b: 2 } }) => 0) | ((x: { foo: { c: 3 } }) => 0)) extends ((x: infer R) => 0) ? R : never> =
Unfoo<{ foo: { a: 1 } | { b: 2 } } & { foo: { c: 3 } }> =
({ foo: { a: 1 } | { b: 2 } } & { foo: { c: 3 } })["foo"] =
({ a: 1 } | { b: 2 }) & { c: 3 } =
{ a: 1 } & { c: 3 } | { b: 2 } & { c: 3 }

希望這也能展示一些其他有用的技巧。

答案#3

jcalz 對這個問題的解答一如既往地完美,不過,對於那些可能不完全理解的人,這裡有他答案中這段引述的更多解釋:

在反變位置中,對相同類型變數的多個候選會導致推斷出交集類型。

上述提示實際上是 UnionToIntersection 類型定義中使用的主要技巧,然而,「逆變」這個術語對我來說很模糊,而 Google 搜尋也沒有提供有用的結果,所以讓我們在這裡來說明一下:

根據維基百科:

許多程式語言的型別系統支持子型別。變異(Variance)是指較複雜型別之間的子型別關係與其組件之間的子型別關係的相關方式。例如,貓的列表應該如何與動物的列表相關?或者,一個返回貓的函數應該如何與一個返回動物的函數相關?

讓我們在程式碼中看看上述的說明:

type A = (a: Animal) => void
type C = (d: Cat) => void
declare let aFunc: A;
declare let cFunc: C;

現在你預期下面哪一個作業是正確的?

cFunc = aFunc // 🚩 [ ]  ✅ [ ]
aFunc = cFunc // 🚩 [ ]  ✅ [ ]

先猜一次,然後再繼續閱讀。:)

與我們對「繼承」可能的預期相反,在繼承中,子類型的值可以指派給其父類型的變量,但當這個變量是函數類型的參數時,並且我們在指派「函數」(維基百科所稱的「複合類型」)時,情況就不是這樣。有趣的是,反方向確實是正確的!也就是說,上面程式片段中 cFunc=aFunc 是正確的,而 aFunc=cFunc 是錯誤的。

現在讓我們來看看為什麼 cFunc=aFunc 是正確的。 它正確的原因是,當我們將某個 Y 類型的變數賦值給某個 X 類型的變數時,只有在新類型(此例中的 Y)不破壞舊類型(此例中的 X)的任何使用情況時,它才會是「正確」的。例如:

a = new Animal()
c = new Cat()
a = c   // ✅ Not breaking, everywhere an Animal is used, a Cat must be useable too
        // (It is also formally known as the "Liskov Substitution Principle".)
a.eat() // ---> c.eat() ✅ No problem, Cats can eat too

現在在函數類型的情況下使用同樣的規則:如果你將一個函數類型為 Foo 的函數 foo 賦值給一個函數類型為 Bar 的變數 bar,那麼無論你在哪裡使用 bar,它仍然必須保持可用/有效。

declare let foo: (param: Animal): void
declare let bar: (param: Cat): void
a = new Animal()
c = new Cat()

// valid usages of foo:
foo(a) // ✅
foo(c) // ✅

// valid usage of bar:
bar(c) // ✅

foo = bar // ❌ wrong because 👇
foo(a)    // ❌ this one has not remained useable / valid
          // because foo expects a Cat now, but is receiving an Animal, which is not valid
foo(c)    // ✅

bar = foo // ✅ correct because 👇 all usages of bar remains still useable / valid
bar(c)    // bar expects an Animal now, and has received a Cat, which is still valid
          // ⭐ That's why we say function parameter is a **contra-variant** position for
          // a type, because it reverses the direction of the assignability.

所以現在我們可以理解為什麼 cFunc=aFunc 是正確的選擇!

這個有趣的邊緣情況是,你可以將函數參數的類型設為 never,這樣你就可以將具有該參數任何類型的函數分配給它:

type Foo = (a: never) => void
type Bar = (a: Function) => void
type Baz = (a: boolean) => void
type Qux = (a: SuperComplexType) => void
declare let foo: Foo
declare let bar: Bar
declare let baz: Baz
declare let qux: Qux
foo = bar // ✅
foo = baz // ✅
foo = qux // ✅

使用相同的貓和動物範例,三種協變/逆變/不變的總結如下:

  • 協變 (Covariance):() => Cat 可以指派給 () => Animal,因為 Cat 可以指派給 Animal;它「保持了可指派性的方向」。
  • 逆變:(Animal) => void 可以指派給 (Cat) => void,因為期望一個 Animal 的東西也能接受一個 Cat;它「反轉了可指派性的方向」。
  • 不變性:(Animal) => Animal 無法指派給 (Cat) => Cat,因為並非所有返回的 Animal 都是 Cat,而 (Cat) => Cat 也無法指派給 (Animal) => Animal,因為期望 Cat 的地方不能接受其他種類的 Animal。

現在這就是 jcalz 的 UnionToIntersection 如何運作的方式:

type FirstHalfOfUnionToIntersection<U> = U extends any ? (k: U)=>void : never

這是一個分佈式條件(因為 extends 之前的類型 U 是裸類型(單獨出現,並不是更複雜類型表達式的一部分)),因此會對聯集的每個組件執行條件,例如,對於 X | Y | Z,它會產生 ((k: X) => void) | ((k: Y) => void) | ((k: Z) => void)。

在這種類型的後半部分,它實際上是在做這個:

<A_union_of_some_functions_from_first_half> extends ((k: infer I)=>void) ? I : never

這又是一個分佈式條件類型,不過,這裡有一個有趣的部分:被推斷的類型 I 處於反變位置(它是一個函數參數),所以它的所有可能推斷將會被取交集!

在反變位置中,同一類型變數的多個候選者會導致推斷出交集類型。

例如,延續相同的 X | Y | Z 範例,結果將會是 X & Y & Z。