How to transform union type to tuple type

DISCLAIMER: DON'T DO THIS!! If someone tells you to use the code they found in this answer to do anything except demonstrate why this is a bad idea, RUN AWAY!!


It's easy to convert from a tuple type to a union type; for example, see this question. But the opposite, converting from a union to a tuple is one of those Truly Bad Ideas that you shouldn't try to do. (See microsoft/TypeScript#13298 for a discussion and canonical answer) Let's do it first and scold ourselves later:

// oh boy don't do this
type UnionToIntersection<U> =
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
type LastOf<T> =
  UnionToIntersection<T extends any ? () => T : never> extends () => (infer R) ? R : never

// TS4.0+
type Push<T extends any[], V> = [...T, V];

// TS4.1+
type TuplifyUnion<T, L = LastOf<T>, N = [T] extends [never] ? true : false> =
  true extends N ? [] : Push<TuplifyUnion<Exclude<T, L>>, L>

type abc = 'a' | 'b' | 'c';
type t = TuplifyUnion<abc>; // ["a", "b", "c"] 

Playground link

That kind of works, but I really really REALLY recommend not using it for any official purpose or in any production code. Here's why:

  • You can't rely on the ordering of a union type. It's an implementation detail of the compiler; since X | Y is equivalent to Y | X, the compiler feels free to change one to the other. And sometimes it does:

      type TypeTrue1A = TuplifyUnion<true | 1 | "a">; // [true, 1, "a"] 🙂
      type Type1ATrue = TuplifyUnion<1 | "a" | true>; // [true, 1, "a"]!! 😮
    

    So there's really no way to preserve the order. And please don't assume that the output will at least always be [true, 1, "a"] above; there's no guarantee of that. It's an implementation detail and so the specific output can change from one version of TypeScript to the next, or from one compilation of your code to the next. And this actually does happen for some situations: for example, the compiler caches unions; seemingly unrelated code can affect which ordering of a union gets put into the cache, and thus which ordering comes out. Order is not simply not reliable.

  • You might not be happy with what the compiler considers a union and when it collapses or expands. "a" | string will just be collapsed to string, and boolean is actually expanded to false | true:

      type TypeAString = TuplifyUnion<"a" | string>; // [string]
      type TypeBoolean = TuplifyUnion<boolean>; // [false, true]
    

    So if you were planning to preserve some existing number of elements, you should stop planning that. There's no general way to have a tuple go to a union and back without losing this information as well.

  • There's no supported way to iterate through a general union. The tricks I'm using all abuse conditional types. First I convert a union A | B | C into a union of functions like ()=>A | ()=>B | ()=>C, and then use an intersection inference trick to convert that union of functions into an intersection of functions like ()=>A & ()=>B & ()=>C, which is interpreted as a single overloaded function type, and using conditional types to pull out the return value only grabs the last overload. All of that craziness ends up taking A | B | C and pulling out just one constituent, probably C. Then you have to push that onto the end of a tuple you're building up.

  • Less of a reason now that TypeScript 4.1 has introduced recursive conditional types, but recursive conditional types are more taxing on the compiler than you might want and have shallow recursion limits. So if you have unions with more than about 20 elements, or unions whose types are recursive in some way themselves, you might see some performance or compilation problems.

So there you go. You can kind of do it, but don't do it. (And if you do do it, don't blame me if something explodes. 💣) Hope that helps. Good luck!


I've sometimes faced a situation in which I want to derive type B from type A but find that either TS does not support it, or that doing the transformation results in code that's hard to follow. Sometimes, the choice of deriving B from A is arbitrary and I could just as well derive in the other direction. Here if you can start with your tuple, you can easily derive a type that covers all the values that the tuple accepts as elements:

type X = ["a", "b", "c"];

type AnyElementOf<T extends any[]> = T[number];

type AnyElementOfX = AnyElementOf<X>;

If you inspect the expansion of AnyElementOfX you'll get "a" | "b" | "c".

Tags:

Typescript