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'];
Union types are used when a value can be more than a single type. Such as when a property would be string or number .
A tuple is a TypeScript type that works like an array with some special considerations: The number of elements of the array is fixed. The type of the elements is known. The type of the elements of the array need not be the same.
In TypeScript, a union type variable is a variable which can store multiple type of values (i.e. number, string etc). A union type allows us to define a variable with multiple types. The union type variables are defined using the pipe ( '|' ) symbol between the types.
A "union declaration" specifies a set of variable values and, optionally, a tag naming the union. The variable values are called "members" of the union and can have different types.
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"
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With