Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

730 - Union to Tuple #737

Open
uid11 opened this issue Jan 25, 2021 · 12 comments
Open

730 - Union to Tuple #737

uid11 opened this issue Jan 25, 2021 · 12 comments
Labels
730 answer Share answers/solutions to a question en in English

Comments

@uid11
Copy link
Contributor

uid11 commented Jan 25, 2021

TS Playground.

/**
 * UnionToIntersection<{ foo: string } | { bar: string }> =
 *  { foo: string } & { bar: string }.
 */
type UnionToIntersection<U> = (
  U extends unknown ? (arg: U) => 0 : never
) extends (arg: infer I) => 0
  ? I
  : never;

/**
 * LastInUnion<1 | 2> = 2.
 */
type LastInUnion<U> = UnionToIntersection<
  U extends unknown ? (x: U) => 0 : never
> extends (x: infer L) => 0
  ? L
  : never;

/**
 * UnionToTuple<1 | 2> = [1, 2].
 */
type UnionToTuple<U, Last = LastInUnion<U>> = [U] extends [never]
  ? []
  : [...UnionToTuple<Exclude<U, Last>>, Last];
@uid11 uid11 added answer Share answers/solutions to a question en in English labels Jan 25, 2021
@github-actions github-actions bot added the 730 label Jan 25, 2021
@xianshenglu
Copy link
Contributor

Could you please add some explanation for the questions below?

type h = ((x: 1) => 0) & ((x: 2) => 0) //why h not never
type e = (((x: 1) => 0) & ((x: 2) => 0)) extends (x: infer L) => 0 ? L : never; //  why e is 2 not never or 1?

It stops me from understanding the answer.

@uid11
Copy link
Contributor Author

uid11 commented Mar 5, 2021

type h = ((x: 1) => 0) & ((x: 2) => 0) //why h not never

Function arguments are in contravariant positions, so when functions intersect, arguments do not intersect, but are united.
This intersection of functions forms an overload -- a function that takes either 1 or 2 as its first argument.

type e = (((x: 1) => 0) & ((x: 2) => 0)) extends (x: infer L) => 0 ? L : never; //  why e is 2 not never or 1?

This is a feature of TS, mentioned somewhere in the documentation -- if it is necessary to output one type from overload, TS selects the last signature ((x: 2) => 0) in the overload.

@xianshenglu
Copy link
Contributor

Thanks so much!

@zojize
Copy link

zojize commented Apr 30, 2021

I was just fiddling around with this question and came up with an answer that 'worked' but really shouldn't have 😂

type UnionToTuple<T> = { [P in T as string]: [P] }[string];

@Jayatubi
Copy link

Jayatubi commented Aug 27, 2021

type h = ((x: 1) => 0) & ((x: 2) => 0) //why h not never

Function arguments are in contravariant positions, so when functions intersect, arguments do not intersect, but are united.
This intersection of functions forms an overload -- a function that takes either 1 or 2 as its first argument.

type e = (((x: 1) => 0) & ((x: 2) => 0)) extends (x: infer L) => 0 ? L : never; //  why e is 2 not never or 1?

This is a feature of TS, mentioned somewhere in the documentation -- if it is necessary to output one type from overload, TS selects the last signature ((x: 2) => 0) in the overload.

If I understand correctly the idea was to first convert union to intersection, and then extract the last element from intersection by the function overloading feature?

@edisonLzy
Copy link

This is a feature of TS, mentioned somewhere in the documentation -- if it is necessary to output one type from overload, TS selects the last signature ((x: 2) => 0) in the overload.

so where's the mentioned in the documentation ?

@plneple
Copy link

plneple commented Mar 5, 2022

  Steps to easier understand this:

  UnionToTuple<'a' | 'b'>
  |  LastInUnion<'a' | 'b'>
  |  |  UnionToIntersection<((x: 'a') => 0) | ((x: 'b') => 0)>
  |  |  | Why: CONTRA-VARIANT position (function overload)
  |  |  Resolve to ((x: 'a') => 0) & ((x: 'b') => 0)
  |  | infer L -> gets 'b' because it's last overload
  |  | DOC: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html
  |  Resolve to 'b'
  |  UnionToTuple<'a'>
  |  |  LastInUnion<'a'>
  |  |  |   UnionToIntersection<(x: 'a') => 0>
  |  |  |   |
  |  |  |   Resolve to (x: 'a') => 0
  |  |  | infer L -> 'a'
  |  |  Resolve to 'a'
  |  |  UnionToTuple<never>
  |  |  |
  |  |  Resolve to []
  |  Resolve to [...[], 'a'], so ['a']
  Resolve to [...['a'], 'b'], so ['a', 'b']

  Success :)

@Alexsey
Copy link
Contributor

Alexsey commented Jun 6, 2022

Thanks to everyone for the explanations!

Still, there is one thing that remains unclear:

intersection of functions forms an overload

Where has this been specified?

I think it's the first time I see where in TS overloads are getting somehow connected to the rest of the type system. In a very unobvious and unsound way - A & B appeared to be not the same as B & A!

UPD: It looks like it is indirectly mentioned in the docs here:

When inferring from a type with multiple call signatures (such as the type of an overloaded function), inferences are made from the last signature (which, presumably, is the most permissive catch-all case). It is not possible to perform overload resolution based on a list of argument types.

Where "type with multiple call signatures" also means functions intersection

@dimitropoulos
Copy link
Contributor

Something changed after TypeScript 2.8 (the doc being mentioned above). The last line isn't string & number as it was at the time, it's never today (I'm in 4.9). Not sure when this changed.

Screenshot_20230122_104123

@dimitropoulos
Copy link
Contributor

dimitropoulos commented Jan 22, 2023

And to add to what @Alexsey said it does seem that through the method found in this solution you can create a scenario where union/intersection order matters:

@Alexsey
Copy link
Contributor

Alexsey commented Jan 22, 2023

Something changed after TypeScript 2.8 (the doc being mentioned above). The last line isn't string & number as it was at the time, it's never today (I'm in 4.9). Not sure when this changed.

Screenshot_20230122_104123

TS didn't change - string & number was always never 😄 It could have been changed the way types are represented in the editor (it may not have been collapsing it to never in "view" mode), but probably it's more of a comment in the doc to show that never is coming from string & number

@dimitropoulos
Copy link
Contributor

ohhh ok. I was just confused because the docs say string & number, but I see now what you mean is that they may have been trying to convey (by implication) that that's never. wow. great. thanks for explaining!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
730 answer Share answers/solutions to a question en in English
Projects
None yet
Development

No branches or pull requests

9 participants