microsoft/TypeScript

Get a list with all values from an Enum

alex321 opened this issue ยท 18 comments

Feature:
If we have an Enum, add the ability to access all of it's values as a list (or any other iterable).

enum Colour { red = 'RED', green = 'GREEN' }

Colour.toList() === ['RED', 'GREEN']

We could have a separate call for the keys? Or vice versa?

It is quite useful as you'd frequently want to be able to iterate over them somehow. Thanks!

Non-const enums can be iterated over. With string enums this is easy because there is no reverse mapping from values to keys, so Object.keys(Colour).map(k => Colour[k as any]) is ['RED', 'GREEN'].

A number enum will map from keys to values and from values to keys, so you must be careful:

enum E { A, B }
const keys = Object.keys(E).filter(k => typeof E[k as any] === "number"); // ["A", "B"]
const values = keys.map(k => E[k as any]); // [0, 1]

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

in case of string enum, simple Object.keys should do it

export enum ShipmentPickUpTime {
  PUT_NONE = "PUT_NONE",
  PUT_ANYTIME = "PUT_ANYTIME",
  PUT_MORNING = "PUT_MORNING",
  PUT_AFTERNOON = "PUT_AFTERNOON",
  PUT_NIGHT = "PUT_NIGHT",
}

const keys = Object.keys(ShipmentPickUpTime);

console.log(keys);
// output
[ 'PUT_NONE',
  'PUT_ANYTIME',
  'PUT_MORNING',
  'PUT_AFTERNOON',
  'PUT_NIGHT' ]
stieg commented

Everything that is said above is accurate for enum types. But what about const enum types? It seems there is a design shortcoming here because there is no way to get a list of all values without changing const enum to a plain old enum. This is a problem because there are cases where a developer would want to use a const enum but can not because they can not programatically get a list of all possible values.

In example suppose a developer wants to use a const enum to represent a field in a mongoose schema. Here is an example of what this code might look like:

const enum Sex {
  FEMALE = "Female",
  MALE = "Male",
}

interface IUser  {
  firstName: string,
  lastName: string,
  sex: Sex,
}

If this developer were to try and create a Mongoose schema, they would not be able to enforce strong types on this sex field they would fail because they would not have access to all the possible const enum values.

export const UserSchema: Schema = new Schema({
  firstName: String,
  lastName: String,
  sex: {
    enum: Object.values(Sex),   // Won't Work because Sex is optimized out.
    type: String,
  },
}, {});

The only way for me to get this to work is to remove the const declaration from the Sex enum. That is not what I want to do because I have no intention of adding additional Sex values to that enum. Thus this enum should be able to be declared as a const enum but can not be because of said aforementioned reasons. This seems like a bug or an oversight. What should be possible here is for me to call some special reserved property on the enum that will return a list of all keys for that enum. In example I would expect something like the following to work in the ideal scenario:

export const UserSchema: Schema = new Schema({
  firstName: String,
  lastName: String,
  sex: {
    enum: Sex._values,
    type: String,
  },
}, {});

This would allow me to use const enum types and also would allow me to know all possible values of the enum itself. Thoughts?

@stieg Maybe make a suggestion issue for that?

That is not what I want to do because I have no intention of adding additional Sex values to that enum.

This really isn't what const is about. I recommend reading https://stackoverflow.com/questions/28818849/how-do-the-different-enum-variants-work-in-typescript

stieg commented

@andy-ms Yeah I can open a new issue if needed. Figured it would be better to first drop this here since this is related to the topic at hand.

@RyanCavanaugh Perhaps you could further elaborate on your point better since I don't seem to understand what point you are trying to make. I am aware of how Typescript handles const enum objects by inlining them when it transpiles them; that is partially what my whole point is here. const enum values can be inlined because there are not supposed to have any further additions. This enables TS to do the optimization here. In the case of normal enum types, you have to use the lookup table because its possible to add other enum later in the code. But what I feel you are missing is the meaning of the modifier const. In pretty much all languages (including TS) its meaning is "does not change". And that is the primary intention and use of the const modifier. The fact that you can apply optimizations as a result of its presence is a secondary benefit. I should be able to use a const enum for the above code but because of how TS optimizes it I am unable to do so. This is why I feel its an oversight in the design of the const enum. Effectively the TS transpiler should provide a way for me to get the values (or should disable the optimization so I can access the list of values). I would much prefer the former.

stieg commented

@andy-ms Filed #21391 to track the issue separately.

`export enum Status{
received = 'RECEIVED',
awaiting = 'AWAITING',
inProgress = 'IN PROGRESS'
}

Object.keys(Status).forEach(key => {
console.log(Interval[key]);
});

Output:>> RECEIVED
AWAITING
IN PROGRESS`

@Kevin2iBi
Everything was great, until some 'Interval' popped in ๐Ÿ’ƒ

@ainouss where you have "Interval" you can replace with "Status".

Has this changed at all? I've tried doing what is posted above with no luck. Console logging shows it as undefined always.

export enum Names{
ownerName = 'John Smith',
clientName = 'Jane Doe',
workerName = 'James Bond'
}

const nameList = Object.keys(Names).forEach(key => {Names[key]});
console.log(nameList);

CONSOLE: undefined

@WholemealDrop you want map not forEach! You also need to return from the function body. Probably best to go to StackOverflow

@RyanCavanaugh I see that now and it is working. Came here because this was the only spot I could find that discussed it. Thanks!

I've found Object.entries to work! Depending on your tsconfig, you'll need to add es2017.object.

// Add to tsconfig.json
"lib": ["es2017.object"],

Here's an example:

export enum REGIONS {
  auckland = "Auckland";
  waikato = "Waikato";
}

// no need to use map, if all you need are key-value tuples: [['auckland', 'Auckland'], ['waikato', 'Waikato']]
export const REGION_OPTIONS = Object.entries(REGIONS);

Seems the only way to get string enum values right now is as follows:

enum Foo {
 A = "the letter A",
 B = "the letter B"
}

fooKeys:string[] = Object.keys(Foo)
fooValues:Foo[] = fooKeys.map(k => Foo[k as any]).map(v => v as Foo)

This is really not ideal, I would expect TS to provide some syntactic sugar to shortcut the latter.

mikob commented

another workaround if you want just the enum strings (can easily be modified to get just the numbers too). Use Object.values since that can return any data type, unlike Object.keys which only returns strings.

enum JA_DICT_DATA {
    'base',
    'cc',
    'check',
    'tid',
    'tid_map',
    'tid_pos',
    'unk_char',
    'unk_compat',
    'unk',
    'unk_invoke',
    'unk_map',
    'unk_pos',
};
const JA_DICTS: JA_DICT_DATA[] = Object.values(JA_DICT_DATA).filter(x => typeof x === 'string');

You still can't get the type-level values of a string enum as far as i'm aware, for example....

export enum Status{
    received = 'RECEIVED',
    awaiting = 'AWAITING',
    inProgress = 'IN PROGRESS'
}

declare const testKeys: keyof typeof Status  // will give the Keys not the values (correct)
declare const testValues: Status[any] // will give back "string" not 'RECEIVED' | 'AWAIT......