Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

String Union to string Array

Tags:

typescript

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: '♣'
}

Hope that helps.


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]  

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