DISCLAIMER: THIS SORT OF CODE IS A BAD IDEA!! Unions are not meant to be iterable, and requests to "choose" or "single out" an element from a union are almost always declined by the TS team. See microsoft/TypeScript#13298 or this question/answer for more context. If you find yourself reaching for code like this, keep this comment from the TS team dev lead in mind:
See comments above - this is not happening. If you find yourself here wishing you had this operation, PLEASE EXPLAIN WHY WITH EXAMPLES, we will help you do something that actually works instead.
So I'll explain how PopUnion<T>
works, but don't mistake an explanation for an endorsement.
While unions are conceptually unordered (A | B
is indistinguishable from B | A
), the compiler as currently implemented does happen to store unions in some order (the particular order can change depending on seemingly unrelated code). No common union operation does anything that exposes this order.
The UnionToIntersection<T>
type turns unions into intersections, as described in Transform union type to intersection type.
Well, intersections are also conceptually unordered in general (A & B
is generally indistinguishable from B & A
), so on first glance it doesn't seem that converting a union to an intersection would help.
However there is one big exception: Intersections of function types are interpreted as overloaded functions with multiple call signatures. And overloads are order-dependent; e.g., if you call an overloaded function the compiler will more or less walk through the call signatures in order and choose the first one that matches.
So PopUnion<T>
takes the union T
and turns it into a union of functions via the distributive conditional type T extends any ? (x: T) => void : never
, so 1 | 2 | 3
becomes ((x: 1) => void) | ((x: 2) => void) | ((x: 3) => void)
and then uses UnionToIntersection
to turn it into an intersection of functions like ((x: 1) => void) & ((x: 2) => void) & ((x: 3) => void)
, which is equivalent to the overloaded function type
{
(x: 1): void;
(x: 2): void;
(x: 3): void;
}
(but keep in mind that in practice the compiler might store 1 | 2 | 3
internally as, say, 3 | 1 | 2
in which case the resulting overload would have the signatures in a different order).
Now we have something like a handle on an ordering.
The final piece of the puzzle is using conditional type inference to infer a single call signature for the overload above. Conditional type inference with infer
was implemented in microsoft/TypeScript#21496, which mentions:
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).
This is generally considered a limitation (people often want the compiler to select overloads based on some other criterion) but it works for PopUnion
. The inference happens from the last signature (which is (x: 3) => void)
in the example but could be different in practice). And so U
is inferred to be the argument type of the last overload (which is 3
in that example).
To recap: if 1 | 2 | 3
is stored internally by the compiler in that order, then PopUnion<1 | 2 | 3>
first distributes to a union of functions as ((x: 1) => void) | ((x: 2) => void) | ((x: 3) => void)
, which is UnionToIntersection
ed to become ((x: 1) => void) & ((x: 2) => void) & ((x: 3) => void)
, which looks like just (x: 3) => void)
when inferring U
, which is inferred to be 3
.
Therefore PopUnion<T>
does something like: "return the last element in the compiler's internal ordered list of the union members of T
".