180

I have a string union type like so:

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<Suit>({
    hearts: 0,
    diamonds: 0,
    spades: 0,
    clubs: 0
});

export function getAllStringUnionValues<TStringUnion extends string>(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?

Alexander Abakumov
  • 13,617
  • 16
  • 88
  • 129
CodeAndCats
  • 7,508
  • 9
  • 42
  • 60

6 Answers6

292

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 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 = <T extends Lit[]>(...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 type

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 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 type

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:

type SuitTuple = ['hearts', 'diamonds', 'spades', 'clubs'];
const ALL_SUITS: SuitTuple = ['hearts', 'diamonds', 'spades', 'clubs']; // extra/missing would warn you
type Suit = SuitTuple[number];  // union type

Note 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: '♣'
}
starball
  • 20,030
  • 7
  • 43
  • 238
jcalz
  • 264,269
  • 27
  • 359
  • 360
  • 2
    Oh nice job inferring the tuple type. This is definitely the DRYest solution I've seen now. Thanks! Also that's an interesting syntax for inferring a union type from a tuple type (`SuitTuple[number]`). – CodeAndCats Aug 30 '17 at 22:43
  • The other thing that I like about your solution is that it can be used for unions of any type. – CodeAndCats Aug 30 '17 at 22:47
  • For the `tuple` function definition, I receive an error that says, "A rest parameter must be of an array type.ts(2370)". You might have meant this: `export const tuple = (...args: T[]) => args;` instead. – Shaun Luttin Dec 04 '18 at 18:21
  • 1
    @ShaunLuttin you are probably using a version of TypeScript before 3.0. Double check that and get back to me. – jcalz Dec 04 '18 at 19:21
  • 1
    Correct you are. – Shaun Luttin Dec 04 '18 at 21:45
  • 3
    It wasn't clear to me that `SuitTuple[number]` means "put any number here", so for future readers: put any number there and it will give you the union type of all entries, rather than return that specific entry. To make it less confusing (maybe?) for future coders I went with `-1` to make it obviously not related to any real entry. – Dave Aug 04 '19 at 15:18
  • 1
    I would really suggest using `number` and not `-1`, since `number` as the index conveys "use the property type from the numeric index signature", whereas `-1` as the index conveys "use the type of the `"-1"`-keyed property". It may be true that the types `XXX[-1]` and `XXX[number]` are the same when `XXX` is `SuitTuple`, but it's not true in general, so I would avoid it. – jcalz Aug 04 '19 at 17:30
  • Is the ` as const` required at the end, it seems to compile just fine without? – Damian Green Aug 08 '19 at 08:36
  • 4
    It compiles, but `Suit` will become `string` and not a union of literal types. Also, `SuitTuple` will be not be a tuple, if it matters. – jcalz Aug 08 '19 at 15:43
  • 1
    I haven't dug too deep into it but it looks like `const ALL_SUITS = ['hearts', 'diamonds', 'spades', 'clubs'] as const; type Suit = typeof ALL_SUITS[number];` works. If so, there is no need to create the "tuple" type. – LosManos Aug 27 '20 at 16:32
  • 1
    This is a classic SO "accepted" answer. A post that does not answer the question at all. The question was how to convert the union type to a string array. And the answer is: look, here you convert an array to a union type (which is exactly the _other way round_). So just write the array yourself and then obtain the union type from it. WONDERFUL. While this might be possible for the OP, this does *not* answer the original question and still gets 150 upvotes? Maybe the correct answer is: typescript does not support this. But then *this* should be the accepted answer and the workaround a comment. – UniversE Jul 22 '21 at 08:48
  • 2
    @UniversE I've edited to be more explicit about saying union-to-tuple is not really possible (or at least not advisable, with a link to a fragile method that became possible after TS2.8 was released) and why. The workaround is there because the actual goal is to have both a union and a tuple in as DRY a way as possible, and union-to-tuple is probably just the Y in an [XY problem](https://en.wikipedia.org/wiki/XY_problem). An answer that stops at "you can't do it, sorry" wouldn't help achieve the goal. I hope that the edited intro section addresses your concerns, or at least acknowledges them. – jcalz Jul 22 '21 at 13:41
  • @jcalz yes thank you. I agree that the _intended_ question was never "how to convert union type to array" but instead it was something like "how to have both union type and an array-like representation with the same contents without repeating them explicitly". Whole different question than what the title suggests, but yes. To that question, this is indeed a very good answer. – UniversE Jul 23 '21 at 12:25
  • I love this, but how could you use this for type declaration (`.d.ts`) files? If I insert the array definition inside the declaration file, tsc returns an error TS1254: A 'const' initializer in an ambient context must be a string or numeric literal or literal enum reference. – Christoph Thiede Aug 27 '21 at 11:44
  • 1
    If you're just declaring types of things then you can't and don't need to initialize the array at all. That means you need to specify the tuple type instead. So you could use the "original answer" version like `type SuitTuple = ['hearts', 'diamonds', 'spades', 'clubs']; declare const ALL_SUITS: SuitTuple; type Suit = SuitTuple[number];`. – jcalz Aug 27 '21 at 12:43
  • 1
    What on earth does `SuitTuple[number]` mean? What's the name of this technique? Can this technique be found in official documentation? Where else can I use this technique? – Bruce Sun Sep 10 '22 at 03:33
  • It's an [indexed access type](https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html). – jcalz Sep 10 '22 at 03:41
  • Is there a way to create a subtype from the array? Something like this: `type SuitTupleWithoutClubs = Exclude;`? – Jonas P. May 02 '23 at 22:11
29

Update for TypeScript 3.4:

There will be a more concise syntax coming with TypeScript 3.4 called "const contexts". It is already merged into master and should be available soon as seen in this PR.

This feature will make it possible to create an immutable (constant) tuple type / array by using the as const or <const> keywords. Because this array can't be modified, TypeScript can safely assume a narrow literal type ['a', 'b'] instead of a wider ('a' | 'b')[] or even string[] type and we can skip the call of a tuple() function.

To refer to your question

However the problem is the type signature for the constant ALL_SUITS is ('hearts' | 'diamonds' | 'spades' | 'clubs')[]. (... it should rather be) ['hearts', 'diamonds', 'spades', 'clubs']

With the new syntax, we are able to achieve exactly that:

const ALL_SUITS = <const> ['hearts', 'diamonds', 'spades', 'clubs'];  
// or 
const ALL_SUITS = ['hearts', 'diamonds', 'spades', 'clubs'] as const;

// type of ALL_SUITS is infererd to ['hearts', 'diamonds', 'spades', 'clubs']

With this immutable array, we can easily create the desired union type:

type Suits = typeof ALL_SUITS[number]  
ggradnig
  • 13,119
  • 2
  • 37
  • 61
8

Easy and right in the heart.

String Union to string Array - the proper decision!

type ValueOf<T> = T[keyof T];

type NonEmptyArray<T> = [T, ...T[]]

type MustInclude<T, U extends T[]> = [T] extends [ValueOf<U>] ? U : never;

function stringUnionToArray<T>() {
  return <U extends NonEmptyArray<T>>(...elements: MustInclude<T, U>) => elements;
}


/* USAGE */
type Variants = "error" | "success" | "info";

// This is what You want!! :)
let stringArray = stringUnionToArray<Variants>()("error", "success", "info");
f v
  • 350
  • 3
  • 5
  • 1
    Best answer by far , especially when the union type comes from a library and you want your array of all values to be in sync _at buildtime_ with the union type. – halogenr Jan 14 '22 at 15:22
  • 42
    Wait, you have to specify all the union type members as parameters to `stringUnionToArray`? But that's exactly what we want to avoid! If that's needed, I can just define the array with those values, instead of going through all that extra code... – Mike Lischke Apr 02 '22 at 11:39
  • 1
    Yes, but it is type-checked. This means that if a new value is ever added to the union you will get a compile-time error: a reminder to update the values. It's not as automatic but it's a strict improvement over hard-coding the values. – negamartin Jun 15 '23 at 02:34
  • 1
    @negamartin normal array can be type-safe too: `let stringArray: Variants[] = ['error', 'success', 'info'];` – marvinfrede Jul 11 '23 at 10:53
4

As @jcalz said, You can not build tuple type for union type because tuple is ordered, when union - not. But, You can build a new union of all possible tuple types that will contain all values of the input union.

Example:

type U2O<U extends string> = {
  [key in U]: U2O<Exclude<U, key>>;
}

type O2T<O extends {}> = {} extends O ? [] : {
  [key in keyof O]: [key, ...O2T<O[key]>];
}[keyof O]

type InputUnion = 'a' | 'b' | 'c'

type UnionOfPossibleTuples = O2T<U2O<InputUnion>>

// Now `UnionOfPossibleTuples` equals to ["a", "b", "c"] | ["a", "c", "b"] | ["b", "a", "c"] | ["b", "c", "a"] | ["c", "a", "b"] | ["c", "b", "a"]

mooksel
  • 133
  • 1
  • 2
  • 7
  • This works only if the `InputUnion` is made of literal types. Something like `type InputUnion = number | string` will not work because in this case `keyof` will return `"toString" | "valueOf"` and lead to an empty result. – tammoj Jul 06 '22 at 11:36
0

Method for transforming string union into a non-duplicating array

Using keyof we can transform union into an array of keys of an object. That can be reapplied into an array.

Playground link

type Diff<T, U> = T extends U ? never : T;

interface IEdiatblePartOfObject {
    name: string;
}

/**
 * At least one key must be present, 
 * otherwise anything would be assignable to `keys` object.
 */
interface IFullObject extends IEdiatblePartOfObject {
    potato: string;
}

type toRemove = Diff<keyof IFullObject, keyof IEdiatblePartOfObject>;

const keys: { [keys in toRemove]: any } = {
    potato: void 0,
};

const toRemove: toRemove[] = Object.keys(keys) as any;

This method will create some overhead but will error out, if someone adds new keys to IFullObject.

Bonus:

declare const safeData: IFullObject;
const originalValues: { [keys in toRemove]: IFullObject[toRemove] } = {
    potato: safeData.potato || '',
};

/**
 * This will contain user provided object,
 * while keeping original keys that are not alowed to be modified
 */
Object.assign(unsafeObject, originalValues);
Akxe
  • 9,694
  • 3
  • 36
  • 71
  • Thanks @Akxe. I found the accepted answer to be the DRYest solution and thus created a small npm package based on it some time ago. You can find it here if interested. https://www.npmjs.com/package/typed-tuple – CodeAndCats Mar 03 '19 at 07:06
0

The accepted answer may not be sufficient if one wants to ensure that a given array matches all the elements of an existing union type.

Here is a solution that uses a function call to ensure that the provided array matches a given union at compile time:

type NoneEmptyArray = readonly any[] & {0: any}
type CompareUnionWithArray<P, Q extends NoneEmptyArray> = Exclude<P, Q[number]> extends never
    ? (Exclude<Q[number], P> extends never ? Q : ReadonlyArray<P>)
    : readonly [...Q, Exclude<P, Q[number]>]
export function assertTypeEquals<P, Q extends NoneEmptyArray>(test: CompareUnionWithArray<P, Q>): void {}
Test Example:
type Suit = 'hearts' | 'diamonds' | 'spades' | 'clubs'

const matchingArray = ['hearts', 'diamonds', 'spades', 'clubs'] as const
const emptyArray = [] as const
const unknownElements = ['hearts', 'diamonds', 'spades', 'clubs', 'UNKNOWN'] as const
const missingElements = ['hearts', 'diamonds', "clubs"] as const

assertTypeEquals<Suit, (typeof matchingArray)>(matchingArray) // no error
assertTypeEquals<Suit, (typeof emptyArray)>(missingElements) // fails because empty array is not allowed
assertTypeEquals<Suit, (typeof unknownElements)>(unknownElements) // fails with: Type '"UNKNOWN"' is not assignable to type 'Suit'.
assertTypeEquals<Suit, (typeof missingElements)>(missingElements) // fails with:
// Argument of type 'readonly ["hearts", "diamonds", "clubs"]' is not assignable to
// parameter of type 'readonly ["hearts", "diamonds", "clubs", "spades"]'.
// Source has 3 element(s) but target requires 4.

Update: Improved the code to not require a useless constant and to generate more informative error messages.

Zomono
  • 772
  • 6
  • 17