Transform union type to intersection type
Asked Answered
A

5

186

Is there a way to transform a union type into an intersection type :

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

I would like to apply a transformation to FunctionUnion to get FunctionIntersection

Agio answered 16/5, 2018 at 15:33 Comment(0)
S
440

You want union to intersection? Distributive conditional types and inference from conditional types can do that. (Don't think it's possible to do intersection-to-union though, sorry) Here's the evil magic:

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

That distributes the union U and repackages it into a new union where all the consitutents are in contravariant position. That allows the type to be inferred as an intersection I, as mentioned in the handbook:

Likewise, multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred.


Let's see if it works.

First let me parenthesize your FunctionUnion and FunctionIntersection because TypeScript seems to bind the union/intersection more tightly than function return:

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

Testing:

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

Looks good!

Be careful that in general UnionToIntersection<> exposes some details of what TypeScript thinks is an actual union. For example, boolean is apparently internally represented as true | false, so

type Weird = UnionToIntersection<string | number | boolean>

becomes

type Weird = string & number & true & false

which in TS3.6+ gets eagerly reduced to

type Weird = never

because it's impossible to have a value which is string and number and true and false.

Shrinkage answered 16/5, 2018 at 15:54 Comment(38)
10x. I always learn new an interesting things from you. I was very close on this question #50369799 but really need a way to transform the union into an intersectionAgio
This answer is awesome but I really find it hard to understand how this part "That distributes the union U and repackages it into a new union where all the consitutents are in contravariant position" works :( I can't fully grasp this contravariant position part. .I thought that this code: type Param<T> = T extends (arg: infer U) => void ? U : never; type InferredParams = Param<((a: string) => void) | ((a: number) => void)>; should give me string & number but it gives me string | number. Can you explain why?Mareah
I thought I would understand this answer when I would "split" it up. Like that: type UnionToIntersectionPart1<U> = U extends any ? (k: U) => void : never; type UnionToIntersectionPart2<U> = U extends ((k: infer I) => void) ? I : never; type IntersectionTest = UnionToIntersectionPart2< UnionToIntersectionPart1<(() => void) | ((p: string) => void)> >; But then it doesn't work! IntersectionTest is still union of function types, not intersection.Mareah
It's because bare type parameters before extends in a conditional type are distributed across any union constituents. If you want to disable distributed conditional types, you can use the trick of making the type parameter "clothed", such as a single-element tuple like this: type Param<T> = [T] extends [(arg: infer U) => void] ? U : never;. That should work the way you want.Shrinkage
Could you explain the beginning part with U extends any ? ? I tried just ((a: U) => void) extends (a: infer I) => void ? I : never; but it doesn't work, even though the type U => void that's generated should be the same.Phytobiology
@RanLottem the key is distributive conditional types. The handbook explains it pretty well, in my opinion. I've expanded on it elsewhere you need more info. Good luck!Shrinkage
@Shrinkage is there a way to use this to get the intersection of union types? type X = (string | null) & (string | number); /* string */ type Y = UnionToIntersection<(string | null) | (string | number)>; /*null & string & number */Bantling
Unions are associative, so (string | null) | (string | number) is just one big union string | number | null. There's nothing for UnionToIntersection<> to act on; it's too late. If you have the chance to alter the original elements before uniting them, you can do it (e.g., UnionToIntersection<[string | null] | [string | number]>[0])Shrinkage
Thank you very much for sharing this trick, it really made things much nicer to work with!Argillaceous
@Shrinkage When properties of the union types have the same name but different types, can we make this somehow pick the type of the property from the first (or last) item in the union? Basically, here's an example: gitter.im/Microsoft/TypeScript?at=5dd06d64e75b2d5a19e5c911Amorphism
Unions in TypeScript are unordered in principle (... and although in practice the compiler does store the constituents in some order, the particular order is not guaranteed to make any sense or be stable from one compile to the next, so you can't rely on it). So any type manipulation that relies on being able to inspect unions for order is a bad idea.Shrinkage
I modified the answer answer above to handle the case of never, which resolves to unknown with the example above. Thanks to @Shrinkage for the tip: type UnionToIntersection<U> = ([U] extends [never] ? never : (U extends any ? ((k: U) => void) : never ) extends ((k: infer I) => void) ? I : never); Lan
@NicholasCuthbert be careful; unknown is the "correct" result. For all A and B, UTI<A | B> should equal UTI<A> & UTI<B>, right? Let B be never and note A | never is A. Now you have UTI<A> = UTI<A> & UTI<never> for all A. The only reasonable way to satisfy that is UTI<never>=unknown, since X & unknown is equal to X. If UTI<never> were never, then UTI<A> = UTI<A> & never, but since X & never=never, you're saying UTI<A> = never for all A... not great. There may be use cases for UTI<never> to be never, but it's not apparent to me, so be careful.Shrinkage
@Shrinkage thanks for the reply. What you say does make sense for the general case. Our use case should be fine since I'm trying to get the intersection of all arguments to a set of functions, and if a single argument is never, that implies that the function should never be called.Lan
@Shrinkage this appears to be broken for Typescript 3.6.3+. The type Weird is evaluating to never. Do you know how I fix it? See typescriptlang.org/play/?ts=3.6.3#code/….Galenical
string & number & true & false really is never, but before TS3.6 the compiler didn't eagerly reduce it. That changed in microsoft/TypeScript#31838. So it's not really something that needs "fixing", I thinkShrinkage
Now this can be implemented just as type UnionToIntersection<U> = [U] extends [infer I] ? I : neverAustralia
For what it's worth, I've put in a feature request to have this as a native generic type: github.com/microsoft/TypeScript/issues/45132Sciomancy
This fails with UnionToIntersection<{ a?: { b?: string }} | { a?: { c?: string }}>, it should return { a?: { b?: string }} & { a?: { c?: string }} but actually returns a never, even though the former is a valid typeLamentable
@Lamentable I can't reproduceShrinkage
@Shrinkage After further testing,it breaks with exactOptionalPropertyTypes turned on: typescriptlang.org/play?exactOptionalPropertyTypes=true#code/… This is a new option in typescript 4.4:typescriptlang.org/docs/handbook/release-notes/…Lamentable
@Lamentable looks like a compiler bug with conditional types and that compiler flag, then, see here. Probably someone should open a GitHub issue about it if there isn't one already.Shrinkage
I'm trying to isolate the problem, looks like function types have some weird behavior around undefined when --exactOptionalPropertyTypes is enabled.Shrinkage
@Lamentable well, looks like it's not about function types at all, but the fact that the compiler is doing weird things with intersections... see this comment I added to ms/TS#45623, an existing bug report. I think it's probably the same, already reported bug, but if not I'll open a new one at some point.Shrinkage
@Lamentable looks like it is fixed by ms/TS#46052 so it probably won't be an issue in TS4.5+Shrinkage
Great answer! Wish they had a less obscure/more direct way to achieve this.Moyra
@Australia Hi,could you please add link in docs or somewhere you learn from to explain why it can be the way you mentioned ? I don't know the relative key words in docs.Camion
@Shrinkage Hi , I really appreciate that I learned a lot from your answers. I'm still a little confused about That distributes the union U and repackages it into a new union where all the consitutents are in contravariant position., namely we got UnionToIntersection<'a' | 'b'> ,according to the distributive property of conditional types and result I think should be (k: 'a')=>void extends ((k: infer I) => void ) ? I : never | (k: 'b')=>void extends ((k: infer I) => void ) ? I : never , that is , 'a' | 'b' , but why 'a' & 'b' ?Camion
@Shrinkage the codes in the docs(typescriptlang.org/docs/handbook/release-notes/…) is like type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never; type T21 = Bar<{ a: (x: string) => void; b: (x: number) => void }>; // string & number in this case, there is no unions or this is another way of union?Camion
In both of those examples, the relevant type is in contravariant position (parameter of a function type). If you have a type like (k: A)=>void | (k: B) => void | (k: C) => void and infer (k: infer I) => void from it, the only reasonable inference is that I is the intersection of A, B, and C. Certainly ((k: A) => void) | (k: B) => void) is not assignable to (k: A | B) => void; see typescriptlang.org/docs/handbook/release-notes/…Shrinkage
@Camion Regarding type UnionToIntersection<U> = [U] extends [infer I] ? I : never, I will have a look at it, but I'm a bit rusty so please bear with me as I try to remember. The reason it works seems to be that for any array [U] where U is a type union, the resulting array item type gets unified to be the type-intersection. Think of it this way, with [string | bigint] the only properties you can safely use are those common to all types. So, while the array can contain any of those, any random element can only the assumed to be string & bigint, and that's what infer I does.Australia
That version does not work. [U] extends [infer I] ? I : never will always be U as far as I can tell. If anyone thinks otherwise, they should demonstrate it.Shrinkage
@Josep although it is nice that jcalz's answer was very helpful to you, please note that if you upvoted more than 2 posts of theirs in a short timespan, you actually set them up for a bad experience because the system will view it as targeted voting and will revert those votes automatically after the reversal script runs the next UTC day. Also please note that serial voting puts you on moderators' radar as this is considered abuse - gladly, your actions are not malicious, so it's unlikely they will incur any penalties, but please do keep this in mind in the future.Hanser
@Josep thanks for the sentiment, but serial voting is not allowed and those votes will likely be reverted soon. You might want to go undo those yourself.Shrinkage
I mean... I'm sorry, and I won't do that again. Although, the answers that I up-voted definitely deserved an up-vote.... Anyways, I think that I'm a pretty advanced TS developer and I was struggling to find a good solution to the problem. I was able to accomplish something similar using recursion on a Tuple, but IMO this solution is way better and I learned a lot... So, it was a way of showing gratitude.Change
@Change just keep the way how the system works in mind in the future - it's O.K. to want to show gratitude for someone whose post helped you out, but it is considered abuse by the system (to prevent an unfortunately common phenomenon of voting fraud and voting rings), and the threshold for triggering it is very low. It is advisable to keep it mind, especially in tag combinations where a small subset of users are very active - it's very easy to run afoul of the system threshold by simply browsing posts and voting casually.Hanser
Why is the infer I needed, and why, once the type is repackaged in the contravariant position, can't we just force the intersection by using U directly?Clandestine
@Shrinkage Thanks for an insightful answer! Is there a way to rewrite UnionToIntersection<U> using explicit variance from TS 4.7? I figure, this might be slightly more verbose, but the code might be more readable, and wouldn't require the "how it works" section.Waxler
A
16

There is also a very related problem when you would like an intersection of several types, but not necessarily convert unions to intersections. There is just no way to get right to intersections without resorting to temporary unions!

The problem is that types we would like to get an intersection of might have unions inside, which will be converted to intersections too. Guards to the rescue:

// 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 }

The execution in the given example goes like this

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 }

Hopefully this also shows some other useful techniques.

Associationism answered 24/12, 2019 at 3:5 Comment(0)
T
7

jcalz' solution to this question is [as always] perfect, however, for those who might not fully understand it, here's a bit more explanation on this quote in his answer:

Multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred.

The above tip is actually the main trick used in the definition of the UnionToIntersection type, however, the term "contra-variant" was vague to me and Googling didn't give helpful results either, so let's go over it here:

According to Wikipedia:

Many programming language type systems support subtyping. Variance refers to how subtyping between more complex types relates to subtyping between their components. For example, how should a list of Cats relate to a list of Animals? Or how should a function that returns Cat relate to a function that returns Animal?

Let's see the above explanation in code:

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

Now which of the below assignments do you expect to be correct?

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

First make a guess, then read on. :)

Opposite of what we might expect from "Inheritance", where values of a subtype could be assigned to a variable of their supertype, this is not true in the same direction when this variable is a parameter of a function type, and we're assigning "functions" (Wikipedia's intended "complex type"). And interestingly, it is indeed correct in the opposite direction! I.e., cFunc = aFunc is the correct one in the snippet above, and aFunc = cFunc is wrong.

Now let's see why cFunc = aFunc is the correct one. The reason it is correct is, when we assign some variable of type Y to some variable of type X, it would be "correct" only if the new type (Y in this example) doesn't break any of the usages of the old type (X in this example). For example:

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

Now use this same rule in case of the function types: If you are assigning a function foo of function type Foo, to a variable bar of function type Bar, then wherever you've used bar, it must remain still useable / valid.

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.

So now we can understand why cFunc = aFunc is the correct choice!

The fun edge case of this is, you can type a function parameter as never and that allows you to assign functions with whatever type for that parameter to it:

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 // ✅

A summary of all the three co/contra/in variances using the same Cat and Animal example is:

  • Covariance: () => Cat is assignable to () => Animal, because Cat is assignable to Animal; It "preserves the direction of the assignability".
  • Contravariance: (Animal) => void is assignable to (Cat) => void, because something that expects an Animal can also take a Cat; It "reverses the direction of the assignability".
  • Invariance: (Animal) => Animal is not assignable to (Cat) => Cat, because not all returned Animals are Cats, and (Cat) => Cat is not assignable to (Animal) => Animal, because something expecting a Cat cannot take any other kind of Animal.

Now this is how jcalz' UnionToIntersection works:

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

This is a distributed conditional (because the type U before extends is a naked type (appears alone and is not part of some more complex type expression)), so runs the conditional for each of the components of the union, e.g., in case of X | Y | Z, it produces ((k: X) => void) | ((k: Y) => void) | ((k: Z) => void).

On the second half of the type, it's actually doing this:

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

This is again a distributed conditional, however, here's the interesting part: The type I that is being inferred is in a contra-variant position (it is a function parameter), so all possible inferences of it will be intersected!

Multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred.

E.g., continuing on the same X | Y | Z example, the result will be X & Y & Z.

Tremaine answered 22/6, 2023 at 9:35 Comment(0)
C
5

I extended @jcalz's answer slightly to get around the boolean issue he described.

type UnionToIntersectionHelper<U> = (
  U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
  ? I
  : never;

type UnionToIntersection<U> = boolean extends U
  ? UnionToIntersectionHelper<Exclude<U, boolean>> & boolean
  : UnionToIntersectionHelper<U>;

this basically prevents it from converting the true | false under the hood to a true & false, preserving the boolean nature of it.

Now it will correctly say UnionToIntersection<boolean> is boolean, not never, while still correctly saying UnionToIntersection<boolean | string> is never

Confounded answered 19/5, 2021 at 18:42 Comment(0)
B
4

You can use UnionToIntersection from a brilliant library utility-types. (1.5M weekly downloads in 2022/11)

Or similar type UnionToIntersection from another gem library type-fest (117M weekly downloads in 2022/11).

Use it as follows:

import type { UnionToIntersection } from 'utility-types'
// or
import type { UnionToIntersection } from 'type-fest'

type FunctionUnion = (() => void) | ((p: string) => void)
type FunctionIntersection = UnionToIntersection<FunctionUnion>
// (() => void) & ((p: string) => void)
Belemnite answered 14/11, 2022 at 17:39 Comment(5)
This answer here is the source for both of those types. This is the 'original' union to intersection :)Agio
what about Type = 0 | 1? I get neverElver
@Elver UnionToIntersection changes 0 | 1 into intersection 0 & 1 which gives never.Belemnite
@Belemnite why that? what about 1 & 2? still never?Elver
@Elver yes, still never. i guess it's because two different constants have no type in common. in practice, result is a type that will satisfy both intersecting types (e.g. 1 & number => 1)Belemnite

© 2022 - 2024 — McMap. All rights reserved.