Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: Iterate through Enum values #4753

Closed
aciccarello opened this issue Sep 11, 2015 · 23 comments
Closed

Suggestion: Iterate through Enum values #4753

aciccarello opened this issue Sep 11, 2015 · 23 comments
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript Too Complex An issue which adding support for may be too complex for the value it adds

Comments

@aciccarello
Copy link

Enums currently get outputted to an object with two-way mapping of numbers. This makes it hard to iterate through the names (or values) of an enum. A method on the enum object to get the keys of the enum would allow iterating.

See this StackOverflow question for a use case of iterating. (And use of a regular expression for filtering).

Possible Solutions:

  • Add a length property (for enums with default index) too limited
  • Function on enum type filtering keys looking for numbers (could break with other index types)
  • Add a function with the keys hard coded when compiling

Complications:

@danquirk
Copy link
Member

I don't think this is a common enough case to emit such a helper for all enums that every TypeScript user writes.

Note that you can merge modules and enums to add methods to the enum yourself if you really do need this functionality. For example, using ES6 generators you can achieve something close to what you want:

enum E {
    A = 2,
    B = 4,
    C = 6
}

module E {
    export function* getvalues() {
        yield E.A;
        yield E.B;
        yield E.C;
    }
}

for(var i of E.getvalues()) {
    console.log(i);
}

@danquirk danquirk added Suggestion An idea for TypeScript Too Complex An issue which adding support for may be too complex for the value it adds Declined The issue was declined as something which matches the TypeScript vision labels Sep 11, 2015
@aciccarello
Copy link
Author

Okay, thanks for the suggestion. I'll see if that is a suitable solution.

@CorayThan
Copy link

Seems like it would be a common use-case to me. I certainly would like that included automatically.

@bladepan
Copy link

It is a common case, for example, you may want to generate a dropdown list from enum dynamically. It is easy to implement too, just add length property to the enum.

(function (MyOptions) {
    MyOptions[MyOptions["option1"] = 0] = "option1";
    MyOptions[MyOptions["option2"] = 1] = "option2";
    MyOptions.length = 2;
})(MyOptions || (MyOptions = {}));

@npiv
Copy link

npiv commented May 19, 2016

I don't understand the argumentation to not include this at all. Too complex maybe, but not a common use case...

What other language has enums and no way to iterate over them?

@RyanCavanaugh
Copy link
Member

no way to iterate over them?

You can iterate over them. See http://stackoverflow.com/a/18112474/1704166 .

@npiv
Copy link

npiv commented May 21, 2016

@RyanCavanaugh

Thank you that does work. My mistake - I misunderstood this issue.

@alamothe
Copy link

alamothe commented May 3, 2018

@danquirk Can't compiler just emit the same code?

It is really sad it's not supported, as every other language with enums have the functionality

@omerman
Copy link

omerman commented Jun 6, 2018

@danquirk I agree with @alamothe so much that I'm leaving this comment saying just that.

@RyanCavanaugh
Copy link
Member

Please don't show up with noise comments.

@saenglert
Copy link

Well, generally how else are people able to show their support but by making some noise for the cause?

This issue is rank 4 on DuckDuckGo and 3 on Google when searching "typescript iterate enum" (which is how I got here as well). I guess wanting to iterate over enums is not an uncommon use case after all and both solutions that were brought up in here are essentially workarounds and quite bulky.

@RyanCavanaugh
Copy link
Member

TypeScript isn't going to emit additional code for every enum just to enable you to do something you can already do by writing code in userland. All the data you need is present at runtime; there's no need for it.

@saenglert
Copy link

I guess by going down that thought we should all be writing and compiling our own assembly code since everything is technically already possible there? /j

I think asking "Can it work without the new feature?" is the wrong approach. The main purpose of a programming language is to make it easy for people to produce solutions. If this new feature accomplishes this, is intuitive (Because let's face it: Both solutions are not unless you are knee deep in the specs) and comes at a reasonable cost: Why not?

There are many examples of "sugar" in JS, basically the whole Array methods are. So forgive me for asking but why is it such a big deal for TS to add aditional functionality? I'm guessing the third part of my before mentioned feature requirements is the problem?

@alamothe
Copy link

I want to add one more technical point. One suggested approach is http://stackoverflow.com/a/18112474/1704166. The problem with it is that it doesn't iterate over values in a type-safe manner - it will produce numbers or strings. So downstream of iteration type safety is lost.

I believe a lot of people who need the iteration are lead to that approach and that's bad. @danquirk's solution is fine and I want to give credit to it.

As for philosophical debate, I think a lot of people who are coming to Typescript from other languages, while in awe of other language features, are left wondering about enum support. Enums in Typescript are not on par with other high-level languages. Personally I don't care what it is compiled down into, as long as it behaves like I expect from an enum type. Maybe it should be reassessed if the right compromise was made between enum usefulness and complexity of the generated code.

@CharlieReitzel
Copy link

CharlieReitzel commented Oct 11, 2019

Edit: This solution works for my use case:
https://stackoverflow.com/questions/52370544/parse-string-as-typescript-enum

It is an evil hack. But it does work generically. I tested it with a numeric/integral enum.

Note: it does not compile in JSFiddle, which does not recognize the keyof operator. But it works with current Typescript in my Angular 8 code and Jasmine unit test. Hth.


For easy and useful approaches, that haven't managed to bog down runtimes, have a look at Java: https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html. Or Python: https://docs.python.org/3/library/enum.html.

To me, there is nothing philosophical about current support for enum iteration in Typescript. It is just broken. It doubles the number enum values, present each one twice: once for the name, and again for the value. There's no clear cut way to distinguish between names and values. Nor any guarantee of order in the language (could break with any TS update). There's no philosophy there to defend. Just a misunderstanding of what enumerated types mean in any scripting language (e.g. Java, Python, ...).

A practical issue I am having is how to deserialize values from the server (via JSON) into actual enum values in my Typescript code. Some evil hack may work yet. But it really should be easy to iterate through enum instances, compare the given value to either the name or value and, if I have a match, use the current enum instance. Currently, this direct approach is not possible.

enum Planet {
    MARS(1),
    VENUS(2),
    EARTH(3),
    MARS(4), ...
    NEPTUNE(8);

    int pno;
    Planet( int pno ) {
        this.pno = pno;
    }
    
    public static Planet findPlanetByNumber( int planetNum ) {
        for (Planet p : Planet.values()) {
            if ( planetNum == p.pno ) {
                return p;
            }
        }
        return null;
    }
}

@newlukai
Copy link

newlukai commented Oct 24, 2019

Not so common use case (?):

enum QueueEvents {
  ITEM_PUSHED = 'item_pushed',
  ITEM_POPPED = 'item_popped'
}
class Queue {
  private readonly eventEmitter: EventEmitter = new EventEmitter();

  // some linked list implementation here
  
  on(event: QueueEvents, listener: (...args: any[]) => void) {
    this.eventEmitter.on(event, listener);
  }
}

Now let's iterate the QueueEvents (in some unit test):

const queue = new Queue();

for (let event in QueueEvents) {
  queue.on(event, () => console.log('emitted ' + event));
}

Doesn't work since event is a string here, not a QueueEvents.

@xiaoxiangmoe
Copy link
Contributor

example here:

function enumToArray<T>(t: T): ReadonlyArray<T[keyof T]> {
  const values = Object.values(t);
  return values.some(x => typeof x === 'number')
    ? values.filter(x => typeof x === 'number')
    : values;
}


enum PlayType {
  tragedy = 1,
  comedy = 2,
}

/**
 * @type readonly PlayType[]
 * @value [1, 2]
 */
const PlayTypeList = enumToArray(PlayType);

enum PlayType2 {
  tragedy = 'tragedy',
  comedy = 'comedy',
}

/**
 * @type readonly PlayType2[]
 * @value ['tragedy', 'comedy']
 */
const PlayTypeList2 = enumToArray(PlayType2);

@CharlieReitzel
Copy link

CharlieReitzel commented Nov 29, 2019

example here:

function enumToArray<T>(t: T): ReadonlyArray<T[keyof T]> {
  const values = Object.values(t);
  return values.some(x => typeof x === 'number')
    ? values.filter(x => typeof x === 'number')
    : values;
}
[ ... snip ... ]
const PlayTypeList2 = enumToArray(PlayType2);

Doesn't work since members of the array are strings or numbers here, not enums. For example, you can't pass them to a function expecting an array of enum values.

For something that does work, see:
https://stackoverflow.com/questions/52370544/parse-string-as-typescript-enum.

@xiaoxiangmoe
Copy link
Contributor

Update:

function enumEntries<T>(t: T): ReadonlyArray<readonly [keyof T, T[keyof T]]> {
  const entries = Object.entries(t);
  const plainStringEnum = entries.every(
    ([key, value]) => typeof value === 'string'
  );
  return (plainStringEnum
    ? entries
    : entries.filter(([k, v]) => typeof v !== 'string')) as any;
}

function enumKeys<T>(t: T): ReadonlyArray<keyof T> {
  return enumEntries(t).map(([key]) => key);
}

function enumValues<T>(t: T): ReadonlyArray<T[keyof T]> {
  const values = Object.values(t);
  const plainStringEnum = values.every(x => typeof x === 'string');
  return plainStringEnum ? values : values.filter(x => typeof x !== 'string');
}

@houfeng0923
Copy link

can enum with number values compile like this :

const Direction = {
  Left: 0,
  Right: 1,
  Down: 2,
  Up: 3,
  '__proto__': {
    0: 'Left',
    1: 'Right',
    2: 'Down',
    3: 'Up'
  }
};

i have a babel plugin test it babel-plugin-iterable-enum

@CharlieReitzel
Copy link

@houfeng0923 I like the looks of that. Nice idea pushing the reverse mapping into the __proto__ space.

@etnbrd
Copy link

etnbrd commented Jan 28, 2020

Is there a reason not to use the enumerable property attribute?
It could only allow the keys of the enum to appear during a for loop or with Object.keys, and hiding the values.
While keeping the forth and back translation between keys and values, as is already the case.

Building upon the previous solutions, I came up with something along the lines of the following code.
for numeric enum only

function iterableEnum<T>(t: T): {[key: string]: T[keyof T]} {
  const keys = Object.keys(t) as [keyof T]
  return keys.reduce((agg, key) => Object.defineProperty(agg, key, {
      value: t[key],
      enumerable: !isFinite(+key)
    }), {})
}

@Ticmea
Copy link

Ticmea commented Sep 17, 2020

I figured I might as well share what I did in order to iterate over enums.

This is probably not the most efficient or elegant way to do this, but it yields values with the correct type, works with both numeric and string enums, is pretty short/straightforward and works for me:

function enumValues<T extends string>(enumObj: { [ key: string ]: T }): IterableIterator<T>;
function enumValues<T extends string | number>(enumObj: { [ key: string ]: T }): IterableIterator<Exclude<T, string>>;
function* enumValues<T>(enumObj: { [ key: string ]: T }): IterableIterator<T> {
  let isStringEnum = true;
  for (const property in enumObj) {
    if (typeof enumObj[property] === 'number') {
      isStringEnum = false;
      break;
    }
  }
  for (const property in enumObj) {
    if (isStringEnum || typeof enumObj[property] === 'number') {
      yield enumObj[property];
    }
  }
}

// use like this; string enums should work too
enum MyEnum { foo, bar, baz }
for (const value of enumValues(MyEnum)) {
  console.info(value); // value is of type MyEnum
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript Too Complex An issue which adding support for may be too complex for the value it adds
Projects
None yet
Development

No branches or pull requests