25

I often use code such as

export type Stuff = 'something' | 'else'
export const AVAILABLE_STUFF: Stuff[] = ['something', 'else']

This way I can both use the type Stuff, and iterate over all the available stuffs if need be.

This works, but it feels like repeating twice the information. And you have to be careful because an update of either Stuff or AVAILABLE_STUFF also requires an update of its counterpart.

Is there a better way to define the type from the Array, or even to somewhat use the array to type some data ?

aherve
  • 3,795
  • 6
  • 28
  • 41
  • Do you want to fill the array with the types or what exactly is the use case to use the array like that? – Murat Karagöz Mar 06 '19 at 10:00
  • 1
    @MuratKaragöz kind of. I would like to be able to profit from the benefits of both `const x: Stuff` and `AVAILABLE_STUFF.forEach(stuff => ...)` with only one declaration. I don't care if I have to declare an array, a type, or whatever else that I yet don't know about :) – aherve Mar 06 '19 at 10:02
  • Have you considered using a string [enum](https://www.typescriptlang.org/docs/handbook/enums.html)? That looks like it should do what you want. It is a type, and you can iterate the values. – sleske Mar 06 '19 at 10:03
  • Closely related to https://stackoverflow.com/questions/51521808/how-do-i-create-a-type-from-a-string-array-in-typescript – devuxer Oct 05 '20 at 17:50

3 Answers3

30

With TypeScript v3.4 const assertions:

export const AVAILABLE_STUFF = <const> ['something', 'else'];
export type Stuff = typeof AVAILABLE_STUFF[number];
ivanjermakov
  • 1,131
  • 13
  • 24
Archit Garg
  • 2,908
  • 2
  • 14
  • 22
14

One built-in option would be to use an enum instead of the type and array approach.

export enum Stuff {
    something = 'something',
    else = 'else',
}

export const AVAILABLE_STUFF: Stuff[] = Object.values(Stuff);

Another option is to extract the type from the type of AVAILABLE_STUFF. To do this we must force the compiler to infer a tuple of string literals for AVAILABLE_STUFF. This can be done in 3.4 with as const or before 3.4 using an extra function. After AVAILABLE_STUFF is a tuple type we can just use a type query to get the type of the elements:

export const AVAILABLE_STUFF = (<T extends string[]>(...o: T)=> o)('something', 'else'); // typed as ["something", "else"]
// export const AVAILABLE_STUFF = ['something', 'else'] as const; // typed as ["something", "else"]  in 3.4
export type Stuff = typeof AVAILABLE_STUFF[number] //"something" | "else"

A few explanations of the above code. typeof AVAILABLE_STUFF gives us the type of the constant (["something", "else"]) to get the [number] is called a type query and will give us the type of an item in the tuple.

The (<T extends string[]>(...o: T)=> o) is just an IIFE we use to force the compiler to infer a string literal tuple type. It has to be generic as the compiler will only infer literal types and tuples in certain cases (a type parameter with a constraint of string being one of them). The as const version is what I would recommend using when it becomes available as it is more readable.

Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • In the second solution, I see that the type `Stuff` is correctly inferred but I don't understand the syntax. What is the role of `[number]` in `typeof AVAILABLE_STUFF[number]`? – Paleo Mar 06 '19 at 10:22
  • 1
    @Paleo added some explanations let me know if it is clearer now. – Titian Cernicova-Dragomir Mar 06 '19 at 10:28
  • Second option works for me, but I'm still puzzled about `enum`. when I try `const x: Stuff = 'something'` I get an error `Type '"something"' is not assignable to type 'Stuff'. ` Am I doing something wrong ? – aherve Mar 06 '19 at 10:48
  • 1
    @aherve it is not a perfect replacement. you will need to use `let a: Stuff = Stuff.something` in TS code. At runtime the value of `a` will be `something` – Titian Cernicova-Dragomir Mar 06 '19 at 10:56
  • @TitianCernicova-Dragomir Thanks for the answer and the explanation. My question is how can I use that approach for a function signature with rest params? For example, `const {bar3} = foo('bar1', 'bar2')` will throw an error, but `const {bar1} = foo('bar1', 'bar2')` will work. (If it should be a different question, let me know and I'll create one) - [Playground](https://mnmlurl.ml/#tMDWm) – Mosh Feu Feb 28 '20 at 07:31
  • 1
    @MoshFeu This should work: https://www.typescriptlang.org/play/?ssl=5&ssc=30&pln=5&pc=22#code/GYVwdgxgLglg9mABBBA3ApgJygHgCqLoAeU6YAJgM6KVSYxgDmAfABQB0nAhpo5QFyI8AbQC6ASkEAldCkzl8AGhp0GLRAG8AUIl2JM6KCExIefdgfIgI6VqwAOB1MojGDYKOMQBeZpp16gY7oqMKumO5QiKI+iADkXGAAnlAAFmpxiFzUiUkA3AGBugZGJojBqAWBAL7KGtVZ1DJyCnjKtPRMzOIF1VpaKGC0WRGxwglxynEARnGiBVBJ9uiI5LGLy3DAI5jCYCAAttNY8-2DwxpZiA3eyGhYUKwTU7M9WkA – Titian Cernicova-Dragomir Feb 28 '20 at 09:10
7

Another possibility is to create an object and use keyof. I recommend that way only if you have anything useful to store as values (in replacement of null in the code below).

const stuffDict = {
  something: null,
  else: null,
}

type Stuff = keyof typeof stuffDict
const AVAILABLE_STUFF = Object.keys(stuffDict) as Stuff[]
Paleo
  • 21,831
  • 4
  • 65
  • 76