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'];
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'];
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"]
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. )
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"
.
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.
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.
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"]