patterntypescriptCriticalCanonical
String Union to string Array
Viewed 0 times
arraystringunion
Problem
I have a string union type like so:
I want a type-safe way to get all the possible values that can be used in this string union. But because interfaces are largely a design-time construct, the best I can do is this:
This works okay, the function ensures I always pass an object where each key is an element in the string union and that every element is included, and returns a string array of all the elements. So if the string union ever changes, the call to this function will error at compile time if not also updated.
However the problem is the type signature for the constant
What I'd really like is a way for my generic
How can I achieve this generically while being as DRY as possible?
type Suit = 'hearts' | 'diamonds' | 'spades' | 'clubs';I want a type-safe way to get all the possible values that can be used in this string union. But because interfaces are largely a design-time construct, the best I can do is this:
export const ALL_SUITS = getAllStringUnionValues({
hearts: 0,
diamonds: 0,
spades: 0,
clubs: 0
});
export function getAllStringUnionValues(valuesAsKeys: { [K in TStringUnion]: 0 }): TStringUnion[] {
const result = Object.getOwnPropertyNames(valuesAsKeys);
return result as any;
}This works okay, the function ensures I always pass an object where each key is an element in the string union and that every element is included, and returns a string array of all the elements. So if the string union ever changes, the call to this function will error at compile time if not also updated.
However the problem is the type signature for the constant
ALL_SUITS is ('hearts' | 'diamonds' | 'spades' | 'clubs')[]. In other words, TypeScript thinks it is an array containing none or more of these values possibly with duplicates, rather than an array containing all the values just once, e.g. ['hearts', 'diamonds', 'spades', 'clubs'].What I'd really like is a way for my generic
getAllStringUnionValues function to specify that it returns ['hearts', 'diamonds', 'spades', 'clubs'].How can I achieve this generically while being as DRY as possible?
Solution
Answer for TypeScript 3.4 and above
It is not really possible to convert a union to a tuple in TypeScript, at least not in a way that behaves well. Unions are intended to be unordered, and tuples are inherently ordered, so even if you can manage to do it, the resulting tuples can behave in unexpected ways. See this answer for a method that does indeed produce a tuple from a union, but with lots of caveats about how fragile it is. Also see microsoft/TypeScript#13298, a declined feature request for union-to-tuple conversion, for discussion and a canonical answer for why this is not supported.
However, depending on your use case, you might be able to invert the problem: specify the tuple type explicitly and derive the union from it. This is relatively straightforward.
Starting with TypeScript 3.4, you can use a
Playground link to code
Answer for TypeScript 3.0 to 3.3
It looks like, starting with TypeScript 3.0, it will be possible for TypeScript to automatically infer tuple types. Once that is released, the
And then you can use it like this:
Answer for TypeScript before 3.0
Since I posted this answer, I found a way to infer tuple types if you're willing to add a function to your library. Check out the function
Using it, you are able to write the following and not repeat yourself:
Original Answer
The most straightforward way to get what you want is to specify the tuple type explicitly and derive the union from it, instead of trying to force TypeScript to do the reverse, which it doesn't know how to do. For example:
Note that you are still writing out the literals twice, once as types in
The advantage here is you don't require key enumeration of a dummy object at runtime. You can of course build types with the suits as keys if you still need them:
It is not really possible to convert a union to a tuple in TypeScript, at least not in a way that behaves well. Unions are intended to be unordered, and tuples are inherently ordered, so even if you can manage to do it, the resulting tuples can behave in unexpected ways. See this answer for a method that does indeed produce a tuple from a union, but with lots of caveats about how fragile it is. Also see microsoft/TypeScript#13298, a declined feature request for union-to-tuple conversion, for discussion and a canonical answer for why this is not supported.
However, depending on your use case, you might be able to invert the problem: specify the tuple type explicitly and derive the union from it. This is relatively straightforward.
Starting with TypeScript 3.4, you can use a
const assertion to tell the compiler to infer the type of a tuple of literals as a tuple of literals, instead of as, say, string[]. It tends to infer the narrowest type possible for a value, including making everything readonly. So you can do this:const ALL_SUITS = ['hearts', 'diamonds', 'spades', 'clubs'] as const;
type SuitTuple = typeof ALL_SUITS; // readonly ['hearts', 'diamonds', 'spades', 'clubs']
type Suit = SuitTuple[number]; // "hearts" | "diamonds" | "spades" | "clubs"Playground link to code
Answer for TypeScript 3.0 to 3.3
It looks like, starting with TypeScript 3.0, it will be possible for TypeScript to automatically infer tuple types. Once that is released, the
tuple() function you need can be succinctly written as:export type Lit = string | number | boolean | undefined | null | void | {};
export const tuple = (...args: T) => args;And then you can use it like this:
const ALL_SUITS = tuple('hearts', 'diamonds', 'spades', 'clubs');
type SuitTuple = typeof ALL_SUITS;
type Suit = SuitTuple[number]; // union typeAnswer for TypeScript before 3.0
Since I posted this answer, I found a way to infer tuple types if you're willing to add a function to your library. Check out the function
tuple() in tuple.ts.Using it, you are able to write the following and not repeat yourself:
const ALL_SUITS = tuple('hearts', 'diamonds', 'spades', 'clubs');
type SuitTuple = typeof ALL_SUITS;
type Suit = SuitTuple[number]; // union typeOriginal Answer
The most straightforward way to get what you want is to specify the tuple type explicitly and derive the union from it, instead of trying to force TypeScript to do the reverse, which it doesn't know how to do. For example:
type SuitTuple = ['hearts', 'diamonds', 'spades', 'clubs'];
const ALL_SUITS: SuitTuple = ['hearts', 'diamonds', 'spades', 'clubs']; // extra/missing would warn you
type Suit = SuitTuple[number]; // union typeNote that you are still writing out the literals twice, once as types in
SuitTuple and once as values in ALL_SUITS; you'll find there's no great way to avoid repeating yourself this way, since TypeScript cannot currently be told to infer tuples, and it will never generate the runtime array from the tuple type.The advantage here is you don't require key enumeration of a dummy object at runtime. You can of course build types with the suits as keys if you still need them:
const symbols: {[K in Suit]: string} = {
hearts: '♥',
diamonds: '♦',
spades: '♠',
clubs: '♣'
}Code Snippets
const ALL_SUITS = ['hearts', 'diamonds', 'spades', 'clubs'] as const;
type SuitTuple = typeof ALL_SUITS; // readonly ['hearts', 'diamonds', 'spades', 'clubs']
type Suit = SuitTuple[number]; // "hearts" | "diamonds" | "spades" | "clubs"export type Lit = string | number | boolean | undefined | null | void | {};
export const tuple = <T extends Lit[]>(...args: T) => args;const ALL_SUITS = tuple('hearts', 'diamonds', 'spades', 'clubs');
type SuitTuple = typeof ALL_SUITS;
type Suit = SuitTuple[number]; // union typeconst ALL_SUITS = tuple('hearts', 'diamonds', 'spades', 'clubs');
type SuitTuple = typeof ALL_SUITS;
type Suit = SuitTuple[number]; // union typetype SuitTuple = ['hearts', 'diamonds', 'spades', 'clubs'];
const ALL_SUITS: SuitTuple = ['hearts', 'diamonds', 'spades', 'clubs']; // extra/missing would warn you
type Suit = SuitTuple[number]; // union typeContext
Stack Overflow Q#44480644, score: 357
Revisions (0)
No revisions yet.