133

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

Titian Cernicova-Dragomir
  • 196,102
  • 20
  • 333
  • 303

3 Answers3

327

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 ? (k: U)=>void : never) extends ((k: 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.

Hope that helps. Good luck!

jcalz
  • 183,886
  • 21
  • 239
  • 259
  • 21
    10x. I always learn new an interesting things from you. I was very close on this question https://stackoverflow.com/questions/50369299/can-i-reuse-the-parameter-definition-of-a-function-in-typescript/50375712#50375712 but really need a way to transform the union into an intersection – Titian Cernicova-Dragomir May 16 '18 at 16:21
  • 5
    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 extends (arg: infer U) => void ? U : never;` `type InferredParams = Param void) | ((a: number) => void)>;` should give me `string & number` but it gives me `string | number`. Can you explain why? – Mariusz Pawelski Jul 21 '18 at 18:26
  • I thought I would understand this answer when I would "split" it up. Like that: `type UnionToIntersectionPart1 = U extends any ? (k: U) => void : never; type UnionToIntersectionPart2 = 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. – Mariusz Pawelski Jul 21 '18 at 18:29
  • 14
    It's because bare type parameters before `extends` in a conditional type are [distributed](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#distributive-conditional-types) 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] extends [(arg: infer U) => void] ? U : never;`. That should work the way you want. – jcalz Jul 22 '18 at 19:02
  • This answer deserves 1000s of upvotes, it almost saved my life! – Nurbol Alpysbayev Dec 20 '18 at 08:08
  • 1
    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. – Ran Lottem May 08 '19 at 20:17
  • 4
    @RanLottem the key is [distributive conditional types](https://github.com/Microsoft/TypeScript/wiki/What's-new-in-TypeScript#distributive-conditional-types). The handbook explains it pretty well, in my opinion. I've [expanded on it elsewhere](https://stackoverflow.com/a/55383816/2887218) you need more info. Good luck! – jcalz May 09 '19 at 13:51
  • @jcalz is there a way to use this to get the intersection of union types? `type X = (string | null) & (string | number); /* string */ type Y = UnionToIntersection; /*null & string & number */` – SamBeran May 15 '19 at 00:28
  • 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[0]`) – jcalz May 15 '19 at 01:10
  • Thank you very much for sharing this trick, it really made things much nicer to work with! – Iazel Oct 02 '19 at 23:00
  • @jcalz 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: https://gitter.im/Microsoft/TypeScript?at=5dd06d64e75b2d5a19e5c911 – trusktr Nov 16 '19 at 21:44
  • 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](https://stackoverflow.com/questions/55127004/how-to-transform-union-type-to-tuple-type). – jcalz Nov 17 '19 at 01:44
  • I modified the answer answer above to handle the case of never, which resolves to unknown with the example above. Thanks to @jcalz for the tip: ```type UnionToIntersection = ([U] extends [never] ? never : (U extends any ? ((k: U) => void) : never ) extends ((k: infer I) => void) ? I : never); ``` – Natalie Cuthbert Feb 11 '20 at 09:17
  • 1
  • @jcalz 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. – Natalie Cuthbert Feb 12 '20 at 16:16
  • @jcalz 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 https://www.typescriptlang.org/play/?ts=3.6.3#code/FAFwngDgpgBAqgOwJYHsEBUUEkEigJwGcoBjEVBAHjgD4YBeGYGFmACjhigA88EATQjACGCMDAD87ANYAueAEoGdAG4ok-GPIRQVBJTz6D2bOTCQIAZgRhYl9Vev5KpWLTB178wUJFgB1KCR8TUZECkwcPCJScjRKQhB8CwBzGAAfDwBXAFsAIxtMvJQUABsoURogA. – iCode101 May 19 '20 at 18:15
  • 1
    `string & number & true & false` really *is* `never`, but before TS3.6 the compiler didn't eagerly reduce it. That changed in [microsoft/TypeScript#31838](https://github.com/microsoft/TypeScript/pull/31838). So it's not really something that needs "fixing", I think – jcalz May 19 '20 at 18:50
  • 1
    Now this can be implemented just as `type UnionToIntersection = [U] extends [infer I] ? I : never` – Trinidad Feb 07 '21 at 08:43
  • For what it's worth, I've put in a feature request to have this as a native generic type: https://github.com/microsoft/TypeScript/issues/45132 – Seph Reed Jul 21 '21 at 05:39
  • This fails with `UnionToIntersection`, it should return `{ a?: { b?: string }} & { a?: { c?: string }}` but actually returns a `never`, even though the former is a valid type – Ferrybig Sep 30 '21 at 13:37
  • 1
    @Ferrybig [I can't reproduce](https://tsplay.dev/NBPZpW) – jcalz Sep 30 '21 at 13:45
  • 1
    @jcalz After further testing,it breaks with [`exactOptionalPropertyTypes`](https://www.typescriptlang.org/tsconfig#exactOptionalPropertyTypes) turned on: https://www.typescriptlang.org/play?exactOptionalPropertyTypes=true#code/C4TwDgpgBAqgdgSwPZwCpIJJ2BATgZwgGNhk4AeGAPigF4AoKKAChiggA8c4ATfKAIZwQUAPwsA1gC5YASjo0AbkgQ8oMuBEV55nbnxbNpUBHABmeKBnm0lKnvPEZ1UTdtz16oSFAAySJAl8ADFTCHQAWWhaWEQUdCwcAmJSFHIAb0FRGUyAI2yofGBcUwBzKABfSqgAHyhMgQLMogKikrhyqoqqegB6ACoob2h-QJCwyOjXLUt+3qA This is a new option in typescript 4.4:https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-4.html – Ferrybig Sep 30 '21 at 13:53
  • 2
    @Ferrybig looks like a compiler bug with conditional types and that compiler flag, then, see [here](https://tsplay.dev/mbd64w). Probably someone should open a GitHub issue about it if there isn't one already. – jcalz Sep 30 '21 at 14:19
  • 2
    I'm trying to isolate the problem, looks like function types have some weird behavior around `undefined` when `--exactOptionalPropertyTypes` is enabled. – jcalz Sep 30 '21 at 14:35
  • 2
    @Ferrybig 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](https://github.com/microsoft/TypeScript/issues/45623#issuecomment-931389122) 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. – jcalz Sep 30 '21 at 14:46
  • 1
    @Ferrybig looks like [it is fixed](https://tsplay.dev/wRJELw) by [ms/TS#46052](https://github.com/microsoft/TypeScript/pull/46052) so it probably won't be an issue in TS4.5+ – jcalz Oct 04 '21 at 19:04
  • 1
    Great answer! Wish they had a less obscure/more direct way to achieve this. – gp-v Dec 09 '21 at 22:49
  • @Trinidad 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. – Archsx Apr 08 '22 at 06:23
  • @jcalz 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` ,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'` ? – Archsx Apr 08 '22 at 18:47
  • @jcalz the codes in the docs(https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types) is like `type Bar = T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never; type T21 = Bar void; b: (x: number) => void }>; // string & number` in this case, there is no unions or this is another way of union? – Archsx Apr 08 '22 at 18:51
  • 1
    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 https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-3.html#improved-behavior-for-calling-union-types – jcalz Apr 08 '22 at 18:56
  • @Archsx Regarding `type UnionToIntersection = [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. – Trinidad May 14 '22 at 00:19
  • That version [does not work](https://tsplay.dev/w8oz0W). `[U] extends [infer I] ? I : never` will always be `U` as far as I can tell. If anyone thinks otherwise, they should demonstrate it. – jcalz May 14 '22 at 00:25
  • 2
    @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. – Oleg Valter is with Ukraine May 31 '22 at 16:47
  • 1
    @Josep thanks for the sentiment, but [serial voting](https://meta.stackexchange.com/q/126829/511366) is not allowed and those votes will likely be reverted soon. You might want to go undo those yourself. – jcalz May 31 '22 at 16:47
  • 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. – Josep May 31 '22 at 19:34
  • @Josep 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. – Oleg Valter is with Ukraine May 31 '22 at 19:40
9

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.

polkovnikov.ph
  • 5,806
  • 4
  • 42
  • 74
0

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

bdwain
  • 1,525
  • 13
  • 32