53

For example, I have a type:

type abc = 'a' | 'b' | 'c';

How to make a tuple type that contains all elements of the union at compile time?

type t = ['a','b', 'c'];
Nail Achmedzhanov
  • 1,264
  • 1
  • 9
  • 12

5 Answers5

111

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.

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. )

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • I've read about intersection interference trick. Compiler infers intersection type with contra-variant, so it should infer union with covarian, right? But this one doesn't make union from interssction type IntersectionToUnion = (U extends any ? U[] : never) extends (infer I)[] ? I : never; Why? – Nail Achmedzhanov Mar 14 '19 at 07:20
  • 4
    Because conditional types distribute over unions, not intersections. Currently nothing in the language distributes over intersections. – jcalz Mar 14 '19 at 10:35
  • It might be bad but sometimes we need to convert type to tuple. Eg., third party library exposes union type for which i need a tuple to make some stuff in my app – JPS Apr 02 '21 at 07:51
  • @JPS well I hope you won't blame me when crazy things happen! – jcalz Apr 02 '21 at 22:03
  • I'd say the union to intersection "trick" is not really a trick but what one would expect from a sound type system (although TS is not sound, in general). It's the overloading thing which is tricky! To avoid the `TuplifyUnion<"a" | string>` and `TuplifyUnion` problem, you can just wrap each term of the union in a function. – Kiuhnm Aug 14 '21 at 02:07
  • Is it the same thing as creating a Union type from an array of values? – SalahAdDin Aug 30 '22 at 16:30
  • This is suitable for determining the size of a union right? Well I suppose I'd have to add an edge case for `boolean`, but aside from that, if I'm not using the tuple for nefarious purposes and only getting the `length`, it should be safe? – kelsny Sep 30 '22 at 15:22
  • I’d think anything invariant to the union order would be safe to use. But I’d still be wary of using it in any production code because of the unsavory mucking around with types necessary to do it. – jcalz Sep 30 '22 at 15:33
  • In my specific use-case, I'm not worried about the order of the tuple. I just have a union of strings and I want to create a strongly typed array that is guaranteed by the compiler to contain all the elements of the union and each element exactly once. For this use-case, I think this solution is acceptable, don't you think? – Evert Nov 23 '22 at 05:52
  • Without more information, I can't be sure, but I'm highly skeptical. If the array exists at runtime then there is no guarantee that the order of the runtime array matches that of the tuple (whose order can change without notice based on seemingly unrelated code), so you'd just be deceiving the compiler. If the array does not exist at runtime then I fail to see the point of the tuple. Your actual underlying use case probably has better solutions, although again, without more information, who knows? This comment section probably isn't the right place to hash that out, though. – jcalz Nov 23 '22 at 15:09
  • This is not that all DANGEROUS in certain cases. For example lets say you have a `foo` object. And you have a `key` variable that you wanna validate with Zod. You can use `z.enum(Object.keys(foo))` but this would error since Zod can't generate any types from this. Then doing `z.enum(Object.keys(foo) as TuplifyUnion)` would be useful. – Shiba Aug 04 '23 at 11:52
  • Or you could write `z.enum(Object.keys(foo) as [keyof typeof foo])` and not play around with pretending to know the order. It isn't dangerous to get such an order if you immediately throw that order away, but I don't see the point. – jcalz Aug 04 '23 at 12:54
2

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".

Louis
  • 146,715
  • 28
  • 274
  • 320
2

Taking jcalz answer a little bit further (again, do not use his answer, all his caveats apply there!), you can actually create something safe and predictable.

// Most of this is jcalz answer, up until the magic.
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;

type Push<T extends any[], V> = [...T, V];

type TuplifyUnion<T, L = LastOf<T>, N = [T] extends [never] ? true : false> =
    true extends N
        ? []
        : Push<TuplifyUnion<Exclude<T, L>>, L>;

// The magic happens here!
export type Tuple<T, A extends T[] = []> =
    TuplifyUnion<T>['length'] extends A['length']
        ? [...A]
        : Tuple<T, [T, ...A]>;

The reason this works is because a tuple in TypeScript is just another Array, and we can compare the length of arrays.

Now, instead of the unpredictable ordering of TuplifyUnion due to compiler behavior, we can create a tuple that is the length of the number of union members where each position is the generic type T.

const numbers: Tuple<2 | 4 | 6> = [4, 6, 2];
// Resolved type: [2 | 4 | 6, 2 | 4 | 6, 2 | 4 | 6]

Admittedly, still imperfect since we can't guarantee that every member is a unique member of the union. But we are now guaranteeing that the count of all members of the union will be covered by the resulting tuple, predictably and safely.

aaron.huggins
  • 543
  • 3
  • 7
0

This approach requires extra work, as it derives the desired tuple type from an actual tuple.

"Um," you are thinking, "what use is it if I have to provide the exact type that I need?"

I agree. However, it's still useful for the related use case: Ensure that a tuple contains all of the elements of a union type (e.g. for unit testing). In this scenario, you need to declare the array anyway, since Typescript cannot produce runtime values from types. So, the "extra" work becomes moot.

Note: As a prerequisite, I'm depending on a TypesEqual operator as discussed here. For completeness, here is the version I'm using, but there are other options:

type FunctionComparisonEqualsWrapped<T> =
  T extends (T extends {} ? infer R & {} : infer R)
  ? { [P in keyof R]: R[P] }
  : never;

type FunctionComparisonEquals<A, B> =
  (<T>() => T extends FunctionComparisonEqualsWrapped<A> ? 1 : 2) extends
   <T>() => T extends FunctionComparisonEqualsWrapped<B> ? 1 : 2
  ? true
  : false;

type IsAny<T> = FunctionComparisonEquals<T, any>;

type InvariantComparisonEqualsWrapped<T> =
  { value: T; setValue: (value: T) => never };

type InvariantComparisonEquals<Expected, Actual> =
  InvariantComparisonEqualsWrapped<Expected> extends
  InvariantComparisonEqualsWrapped<Actual>
  ? IsAny<Expected | Actual> extends true
    ? IsAny<Expected> | IsAny<Actual> extends true
          ? true
          : false
      : true
  : false;

export type TypesEqual<Expected, Actual> =
  InvariantComparisonEquals<Expected, Actual> extends true
  ? FunctionComparisonEquals<Expected, Actual>
  : false;

export type TypesNotEqual<Expected, Actual> =
  TypesEqual<Expected, Actual> extends true ? false : true;

And here is the actual solution with example usage:

export function rangeOf<U>() {
  return function <T extends U[]>(...values: T) {
    type Result = true extends TypesEqual<U, typeof values[number]> ? T : never;
    return values as Result;
  };
}

type Letters = 'a' | 'b' | 'c';
const letters = rangeOf<Letters>()(['a', 'b', 'c']);
type LettersTuple = typeof letters;

Some caveats: rangeOf does not care about ordering of the tuple, and it allows duplicate entries (e.g. ['b', 'a', 'c', 'a'] will satisfy rangeOf<Letters>()). For unit tests, these issues are likely not worth caring about. However, if you do care, you can sort and de-duplicate values before returning it. This trades a small amount of runtime performance during initialization for a "cleaner" representation.

-1

Just a kind of solution. This is quite simple, but requires manually writing of boilerplate code.

Any object with type LazyStyleLoader must contain as properties all members of the ThemeName union.

At least, you can be sure that your object always has a property for each member of the union.

export type ThemeName = "default_bootstrap" | "cerulean" | "cosmo" | "cyborg"

type LazyStyleLoader = {
    [key in ThemeName]: () => Promise<typeof import("*?raw")>
}

export const LazyThemeLoader: LazyStyleLoader = {
    default_bootstrap: () => import("bootstrap/dist/css/bootstrap.min.css?raw"),
    cerulean: () => import("bootswatch/dist/cerulean/bootstrap.min.css?raw"),
    cosmo: () => import("bootswatch/dist/cosmo/bootstrap.min.css?raw"),
    cyborg: () => import("bootswatch/dist/cyborg/bootstrap.min.css?raw"),
};

Then you can get a tuple of union members using Object.keys():

const tuple = Object.keys(LazyThemeLoader);
// ["default_bootstrap", "cerulean", "cosmo", "cyborg"]
James Bond
  • 2,229
  • 1
  • 15
  • 26
  • @OlegValteriswithUkraine, ah, the op is asking about convertiing a union to a tuple in **type** context. My solution is for **expression** context. Anyway, I'll leave it here, maybe it will be usefull for future readers. – James Bond May 13 '22 at 18:54
  • That's ok, I've seen you edited the answer to include the idea of getting a tuple out of it (hence the removal of the note and of the vote). A `typeof` type query is a possibility here. – Oleg Valter is with Ukraine May 13 '22 at 18:57