184

I currently have both an array of strings and a string literal union type containing the same strings:

const furniture = ['chair', 'table', 'lamp'];
type Furniture = 'chair' | 'table' | 'lamp';

I need both in my application, but I am trying to keep my code DRY. So is there any way to infer one from the other?

I basically want to say something like type Furniture = [any string in furniture array], so there are no duplicate strings.

Duncan Lukkenaer
  • 12,050
  • 13
  • 64
  • 97

4 Answers4

335

TypeScript 3.4+

TypeScript version 3.4 has introduced so-called **const contexts**, which is a way to declare a tuple type as immutable and get the narrow literal type directly (without the need to call a function like shown below in the 3.0 solution).

With this new syntax, we get this nice concise solution:

const furniture = ['chair', 'table', 'lamp'] as const;
type Furniture = typeof furniture[number];

More about the new const contexts is found in this PR as well as in the release notes.

TypeScript 3.0+

With the use of generic rest parameters, there is a way to correctly infer string[] as a literal tuple type and then get the union type of the literals.

It goes like this:

const tuple = <T extends string[]>(...args: T) => args;
const furniture = tuple('chair', 'table', 'lamp');
type Furniture = typeof furniture[number];

More about generic rest parameters

ggradnig
  • 13,119
  • 2
  • 37
  • 61
  • 34
    Can I ask, what's the purpose of the index signature annotation `[number]`? Is that not inferred? – robC Jun 19 '19 at 15:45
  • @ggradnig Thanks for the answer! this feels like it was meant to take the place of string enums with reverse mappings, or am I wrong? are there other use cases for this? – Daniel Dror Jun 27 '19 at 14:07
  • 34
    The reason for the `[number]` is that without it `typeof furniture` would return an array type. With the index signature `typeof furniture[number]` is saying "the type of any valid numeric index in furniture, so you get a type that is a union of the values instead of an array type. – Jason Kohles May 19 '20 at 16:43
  • 6
    Unfortunately, this only works with literal arrays. This will not work: `const a = ["a", "b", "c"]; const b = a as const;` - This will throw the following error: `A 'const' assertions can only be applied to references to enum members, or string, number, boolean, array, or object literals.` – Slavik Meltser Aug 03 '20 at 09:52
  • This is an awesome solution to get PHPStorm autocompletion for huge string possibles values for a string parameter. Do you now if there is a way to document each const value to get hints in addition to auto-completion (in jsdoc style) ? – Jordan Breton Feb 22 '21 at 18:39
  • @SlavikMeltser: I think the issue is that your first definition of the array must have the `as const` on it. In your example, a new instruction could be inserted between defining the `a` and defining `b` that changes `a`, so I believe that's why TS doesn't want to trust it. For me, running TS 4.5.2, the following works without issue: `const a = ["x", "y", "z"] as const;` // `const b = a;` If I then look at the TS type of `b`, I get `const b: readonly ["x", "y", "z"]`. – Erdős-Bacon Feb 15 '22 at 17:16
  • @SlavikMeltser @Erdős-Bacon Typescript has implemented rules to infer the correct type of an value if no type annotation is given. For example, `["a", "b", "c"]` is inferred as `string[]`. This is just a choice that was made by the TypeScript designers - it could as well be `const ["a", "b", "c"]`. The reason for that choice is that most of the times, `string[]` is what the developer wants, which is why you need to overrule it by using `as const` if you want to have it another way. – ggradnig Feb 26 '22 at 19:50
  • @SlavikMeltser and yes, a variable typed `string[]` cannot be asserted `as const` after it's declaration. – ggradnig Feb 26 '22 at 19:52
  • @Ariart arrording to https://github.com/microsoft/TypeScript/issues/30445, you can use `const arr2 = /** @type {const} */ ([1, 2, 3])` to document the value correctly in JSDoc – ggradnig Feb 26 '22 at 19:55
  • what happens if the array comes from a array of objects and with `array.map` like: `const furniture = array.map(item => item.name) as const;` this leads to the following error message: `A 'const' assertions can only be applied to references to enum members, or string, number, boolean, array, or object literals.` any idea on how to fix this? @ggradnig – Deniz Nov 26 '22 at 09:17
  • @Deniz TypeScript has to know statically - that means without executing the code - which values are part of the array. So the values must be somewhere "hardcoded". If they are hardcoded somewhere else and you just need to map it to another array, feel free to post a new question on StackOverflow, so we can look at the specific problem. – ggradnig Nov 27 '22 at 16:31
19

This answer is out of date; see @ggradnig's answer.

The best available workaround:

const furnitureObj = { chair: 1, table: 1, lamp: 1 };
type Furniture = keyof typeof furnitureObj;
const furniture = Object.keys(furnitureObj) as Furniture[];

Ideally we could do this:

const furniture = ['chair', 'table', 'lamp'];
type Furniture = typeof furniture[number];

Unfortunately, today furniture is inferred as string[], which means Furniture is now also a string.

We can enforce the typing as a literal with a manual annotation, but it brings back the duplication:

const furniture = ["chair", "table", "lamp"] as ["chair", "table", "lamp"];
type Furniture = typeof furniture[number];

TypeScript issue #10195 tracks the ability to hint to TypeScript that the list should be inferred as a static tuple and not string[], so maybe in the future this will be possible.

Quentin Veron
  • 3,079
  • 1
  • 14
  • 32
Denis
  • 5,061
  • 1
  • 20
  • 22
9

easiest in typescript 3.4: (note TypeScript 3.4 added const assertions)

const furniture = ["chair", "table", "lamp"] as const;
type Furniture = typeof furniture[number]; // "chair" | "table" | "lamp"

also see https://stackoverflow.com/a/55505556/4481226

or if you have these as keys in an object, you can also convert it to a union:

const furniture = {chair:{}, table:{}, lamp:{}} as const;
type Furniture = keyof typeof furniture; // "chair" | "table" | "lamp"
bristweb
  • 948
  • 14
  • 14
-2

The only adjustement I would suggest is to make the const guaranteed compatible with the type, like this:

type Furniture = 'chair' | 'table' | 'lamp';

const furniture: Furniture[] = ['chair', 'table', 'lamp'];

This will give you a warning should you make a spelling error in the array, or add an unknown item:

// Warning: Type 'unknown' is not assignable to furniture
const furniture: Furniture[] = ['chair', 'table', 'lamp', 'unknown'];

The only case this wouldn't help you with is where the array didn't contain one of the values.

Fenton
  • 241,084
  • 71
  • 387
  • 401