230

Say I have an array:

const list = ['a', 'b', 'c']

Is it possible to derive from this value union type that is 'a' | 'b' | 'c'?

I want this because I want to define type which allows only values from static array, and also need to enumerate these values at runtime, so I use array.

Example how it can be implemented with an indexed object:

const indexed = {a: null, b: null, c: null}
const list = Object.keys(index)
type NeededUnionType = keyof typeof indexed

Is it possible to do it without using an indexed map?

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
WHITECOLOR
  • 24,996
  • 37
  • 121
  • 181

5 Answers5

469

CURRENT ANSWER

In TypeScript 3.4 and above, you can use a const assertion to tell the compiler to retain the specific literal types of any literal values in an expression.

const list = ['a', 'b', 'c'] as const; // const assertion
type NeededUnionType = typeof list[number]; // 'a'|'b'|'c';

Playground link to code

UPDATE Feb 2019

In TypeScript 3.4, which should be released in March 2019 it will be possible to tell the compiler to infer the type of a tuple of literals as a tuple of literals, instead of as, say, string[], by using the as const syntax. This type of assertion causes the compiler to infer the narrowest type possible for a value, including making everything readonly. It should look like this:

const list = ['a', 'b', 'c'] as const; // TS3.4 syntax
type NeededUnionType = typeof list[number]; // 'a'|'b'|'c';

This will obviate the need for a helper function of any kind. Good luck again to all!


UPDATE July 2018

It looks like, starting with TypeScript 3.0, it will be possible for TypeScript to automatically infer tuple types. Once 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 list = tuple('a','b','c');  // type is ['a','b','c']
type NeededUnionType = typeof list[number]; // 'a'|'b'|'c'

Hope that works for people!


UPDATE December 2017

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 list = tuple('a','b','c');  // type is ['a','b','c']
type NeededUnionType = typeof list[number]; // 'a'|'b'|'c'

Good luck!


ORIGINAL July 2017

One problem is the literal ['a','b','c'] will be inferred as type string[], so the type system will forget about the specific values. You can force the type system to remember each value as a literal string:

const list = ['a' as 'a','b' as 'b','c' as 'c']; // infers as ('a'|'b'|'c')[]

Or, maybe better, interpret the list as a tuple type:

const list: ['a','b','c'] = ['a','b','c']; // tuple

This is annoying repetition, but at least it doesn't introduce an extraneous object at runtime.

Now you can get your union like this:

type NeededUnionType = typeof list[number];  // 'a'|'b'|'c'.
jcalz
  • 264,269
  • 27
  • 359
  • 360
  • This is an excellent solution for a problem I run into quite often, when I need runtime checks (using the tuple) and compile time checks using a type union. Anyone knows if there are efforts to add support for implicit tuple typing to the language? – Jørgen Tvedt Apr 26 '18 at 07:38
  • Tried your solution but it does NOT work with `cont xs = ['a','b','c']; const list = tuple(...xs);` – Miguel Carvajal Jan 31 '19 at 12:54
  • 2
    It's not supposed to work with that, since by the time you do `const xs = ['a','b','c']` the compiler has already widened `xs` to `string[]` and completely forgotten about the specific values. I can't help that behavior at least as of TS3.2 (there might be a future `as const` notation that works though). Anyway I think the answer as stands is still as correct as I can make it (I mention in there that `['a','b','c']` is inferred as `string[]`) so I'm not sure what more you need. – jcalz Jan 31 '19 at 14:57
  • 16
    can someone explain what `[number]` does in `list[number]` ? – Orelus May 20 '19 at 12:31
  • 13
    It's parsed as `(typeof list)[number]`... not `typeof (list[number])`. The type `T[K]` is a [lookup type](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#keyof-and-lookup-types) which gets the type of the property of `T` whose key is `K`. In `(typeof list)[number]`, you're getting the types of the properties of `(typeof list)` whose keys are `number`. Arrays like `typeof list` have [numeric index signatures](https://www.typescriptlang.org/docs/handbook/interfaces.html#indexable-types), so their `number` key yields the union of all numerically-indexed properties. – jcalz May 20 '19 at 13:10
  • Just a follow up question: Would this solution be correct if the array was dynamically built? – tbuglc Apr 19 '21 at 10:29
  • `['a', 'b', 'c'] as const` gives the type `readonly ["a", "b", "c"]` which might not be what we want. `['a' as const, 'b' as const, 'c' as const]` gives type `("a" | "b" | "c")[]` which is probably what we want. – smokku Jun 08 '22 at 08:16
  • Can someone update for 2022? – Moritz Roessler Nov 24 '22 at 14:31
  • As far as I know there is nothing to update (except possibly for changing future tense to present or past tense); `const` assertions are still the way to do this. – jcalz Nov 25 '22 at 00:42
  • Can this be done from a generic? `function getValue(values: T, predicate: any): QUESTION` What should be in QUESTION to ensure the call to `getValue(['a', 'b'])` limits the return value to `'a' | 'b'`? – Highmastdon Jan 05 '23 at 14:13
  • I found a way: `type GetValueFn = (values: T ) => T[number];` – Highmastdon Jan 05 '23 at 14:24
29

Update for TypeScript 3.4:

A new syntax called "const contexts" that will arrive in TypeScript 3.4 will allow for an even simpler solution that doesn't require a function call as demonstrated. This feature is currently in review as seen in this PR.

In short, this syntax allows for the creation of immutable arrays that have a narrow type (i.e. the type ['a', 'b', 'c'] instead of ('a' | 'b' | 'c')[] or string[]). This way, we can create union types from literals as easy as shown below:

const MY_VALUES = <const> ['a', 'b', 'c']
type MyType = typeof MY_VALUES[number]

In alternative syntax:

const MY_VALUES = ['a', 'b', 'c'] as const
type MyType = typeof MY_VALUES[number]
ggradnig
  • 13,119
  • 2
  • 37
  • 61
6

I assume you are living on TypeScript after March 2019 now. (It's November 2021)

I'm just extending the top answer with an exportable utility function:

// Your handmade utils' library file
export type UnionOfArrayElements<ARR_T extends Readonly<unknown[]>> = ARR_T[number];
// Usage
const a = ["hi", "bye", 3, false] as const;
type ta = UnionOfArrayElements<typeof a>; // false | "hi" | "bye" | 3

const b = [4, 5, 6];
type tb = UnionOfArrayElements<typeof b>; // number

Aidin
  • 25,146
  • 8
  • 76
  • 67
  • 1
    this works great when you have some third-party types of the form `("foo"|"bar")[]` and you want to extract the string-literal union type of possible values. thanks! – Dan O Apr 28 '22 at 18:06
3

It is not possible to do this with Array.

The reason is, even if you declare the variable as const, the content of an array can still change, thus @jonrsharpe mention this is runtime.

Given what you want, it may be better to use interface with keyof:

interface X {
    a: string,
    b: string
}

type Y = keyof X  // Y: 'a' | 'b'

Or enum:

enum X { a, b, c }
unional
  • 14,651
  • 5
  • 32
  • 56
0

If using a an object for storing "constants", this is a way to implement the same idea:

(Note the 'as const' to change the type of keyOne and keyTwo from string to literal.)

const configObj = {
  keyOne: 'literalTypeValueOne' as const,
  keyTwo: 'literalTypeValueTwo' as const,
};

const typeValues = [configObj.keyOne, configObj.keyTwo] as const;
type MyType = typeof typeValues[number];
Bjørnar Hvidsten
  • 859
  • 12
  • 15