2

While skimming through the TypeScript challenges, I came across a particularly interesting one: How to turn a union to an intersection.

Unable to figure it out myself, I turned to the solutions where I found a great approach here and an even greater explanation given here by @jcalz.

The only problem in my way was this question: effectively a user tried to break down the solution in multiple separate statements and, to my surprise, the result was not the same. Instead of foo & bar we were getting foo | bar. Putting the solution back together as a "one-liner" the result gets "restored": foo & bar.

// Type definition
type UnionToIntersection<U> = (U extends any ? (arg: U) => any : never) extends ((arg: infer I) => void) 
  ? I 
  : never;

// As expected  ('foo' & 'bar' is never)
type I = UnionToIntersection<'foo' | 'bar'>;  // never

// Let's break-down `UnionToIntersection`

type A = 'foo' | 'bar';

type B = A extends any ? (arg: A) => any : never // (arg: A) => any;

// This should have been 'foo' & 'bar' (never) just like `type I` 
type C = B extends ((arg: infer I) => void) ? I : never //  'foo' | 'bar'

What's going on here? Shouldn't type I and type C be the same?

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
stratis
  • 7,750
  • 13
  • 53
  • 94

2 Answers2

2

effectively a user tried to break down the solution in multiple separate statements

The user made a mistake. In the original type, U is a generic type which gets distributed in the conditional.

In B, you use A which is not a generic type. No distribution takes place.

If we modify B to also have a generic type U (which we just default to be A), we will see the same result never.

type B<U = A> = U extends any ? (arg: U) => any : never 
type C = B extends ((arg: infer I) => void) ? I : never 
//   ^? never

Playground

Tobias S.
  • 21,159
  • 4
  • 27
  • 45
  • OMG! That's it! Well spotted! Thank you! – stratis Oct 09 '22 at 09:27
  • One last thing: I noticed that changing `C` to this: `type C = U extends ((arg: infer I) => void) ? I : never;` and then doing `type Result = C` will once again return `'foo' | 'bar'`. Do you know why? Thanks again! – stratis Oct 09 '22 at 10:21
  • 1
    @stratis - Its pretty much the opposite problem of before. `B` was not generic, so no distribution which means we have `((arg: "foo") => any) | ((arg: "bar") => any) extends infer I` => `I` is intersection of `"foo" & "bar"`. If `B` is generic, we have `((arg: "foo") => any) extends infer I` and `((arg: "bar") => any) extend infer I` where `I` will be a union of both results – Tobias S. Oct 09 '22 at 10:34
  • Could also use `A extends infer X ? X extends X ? ... : ...`. Although `X` isn't really a "generic" type it still works. – kelsny Oct 09 '22 at 13:59
2

Distributive conditional types work for (naked) parameter types.

Replace your

type B = A extends any ? (arg: A) => any : never // (arg: A) => any;

with

type BT<T> = T extends any ? (arg: T) => any : never;
type B = BT<A>

to see the difference

kikon
  • 3,670
  • 3
  • 5
  • 20