65

How would I map a typescript enum? For example, with strings you can do this:

let arr = [ 'Hello', 'Goodbye' ];

arr.map(v => {
  if (v === 'Hello') {
    return ':)';
  } else if (v === 'Goodbye') {
    return ':(';
  }
); // [ ':)', ':(' ]

This, of course, doesn't work with enums:

enum MyEnum { Hello, Goodbye };

MyEnum.map(v => {
  if (v === MyEnum.Hello) {
    return ':)';
  } else if (v === MyEnum.Goodbye) {
    return ':(';
  }
}); // does not work

Ideally, I'd like to do this in a generalized way so I can simply take any enum I have and put it through a map function while preserving type information. Usage might look something like this:

map(MyEnum, v => {
  if (v === MyEnum.Hello) {
    return ':)';
  } else if (v === MyEnum.Goodbye) {
    return ':(';
  }
}); // [ ':)', ':(' ]

I've been fiddling around with getting a function that does this for me but keep having issues getting the generics just right.

James Monger
  • 10,181
  • 7
  • 62
  • 98
Braden Snell
  • 1,171
  • 2
  • 10
  • 16
  • Another option is to use a union type instead of an enum for easier iterating and other benefits. https://stackoverflow.com/questions/40275832/typescript-has-unions-so-are-enums-redundant/60041791#60041791 – Noumenon Sep 14 '22 at 05:02

8 Answers8

71

To map an enum do this:

(Object.keys(MyEnum) as Array<keyof typeof MyEnum>).map((key) => {})
Justidude
  • 721
  • 5
  • 5
  • 3
    Adding to the above, if you want the keys as an array, use this : ``` Object.keys(MyEnum).filter((el) => { return isNaN(Number(el)) }) ``` – SHARAD Sep 22 '21 at 08:07
  • 3
    typing is better with: `(Object.keys(MyEnum) as Array< MyEnum >` – ValRob Jan 26 '23 at 15:41
18

The function to solve this is quite simple.

// you can't use "enum" as a type, so use this.
type EnumType = { [s: number]: string };

function mapEnum (enumerable: EnumType, fn: Function): any[] {
    // get all the members of the enum
    let enumMembers: any[] = Object.keys(enumerable).map(key => enumerable[key]);

    // we are only interested in the numeric identifiers as these represent the values
    let enumValues: number[] = enumMembers.filter(v => typeof v === "number");

    // now map through the enum values
    return enumValues.map(m => fn(m));
}

As you can see, we first need to get all of the keys for the enum (MyEnum.Hello is actually 1 at runtime) and then just map through those, passing the function on.

Using it is also simple (identical to your example, although I changed the name):

enum MyEnum { Hello, Goodbye };

let results = mapEnum(MyEnum, v => {
  if (v === MyEnum.Hello) {
    return ':)';
  } else if (v === MyEnum.Goodbye) {
    return ':(';
  }
});

console.log(results); // [ ':)', ':(' ]

The reason we need to filter the enum to be numbers only is because of the way enums are compiled.

Your enum is actually compiled to this:

var MyEnum;
(function (MyEnum) {
    MyEnum[MyEnum["Hello"] = 0] = "Hello";
    MyEnum[MyEnum["Goodbye"] = 1] = "Goodbye";
})(MyEnum || (MyEnum = {}));
;

However we are not interested in "Hello" or "Goodbye" as we can't use those at runtime.


You will also notice a funny type statement right before the function. This is because you can't type a parameter as someParameter: enum, you need to explicitly state it as a number -> string map.

James Monger
  • 10,181
  • 7
  • 62
  • 98
  • This is great and led me to a workable solution but it does fall short on preserving type information. Adding some generics would really help. According to [this](http://stackoverflow.com/questions/30774874/enum-as-parameter-in-typescript), however, it looks like using generics with enums isn't _completely_ possible with typescript as of yet. – Braden Snell Jan 03 '17 at 19:02
  • Which type information would you like, specifically, and where? – James Monger Jan 04 '17 at 23:43
  • This function signature is an improvement: `function mapEnum(enumerable: EnumType, fn:(v: any) => T):T[]`. Unfortunately `v` still has a type of any. Ideally, the type of `v` would be inferred from the type of the enumerable. Unfortunately, it doesn't look like this is possible currently. (I'd love to be wrong about that though!) – Braden Snell Jan 06 '17 at 16:45
  • @Braden Snell You can set the `v` type as a number. You could also specify `MyEnum`, but it would require extra generics which I don't think is worth. – Rodris Jul 05 '17 at 13:03
13

Mapping in Typescript can be extremely powerful for writing less code. I have been using key value Enum mapping a lot recently and would recommend it! Here are a couple of examples!

Basic enum usage

enum InlineStyle {
   "Bold",
   "Italic",
   "Underline"
}

type IS = keyof typeof InlineStyle

// Example of looping
(Object.keys(InlineStyle) as Array<IS>).forEach((key) => {
  // code here
})

// Example of calling a function
const styleInline = (style: IS) => {
  // code here
}

Enum key value usage

enum ListStyle {
  "UL" = "List",
  "OL" = "Bullet points"
}

// Example of looping
Object.entries(ListStyle).forEach(([key, value]) => {
  // code here
})

Interface mapping

enum InlineStyle {
   "Bold" = "isBold",
   "Italic" = "isItalic",
   "Underline" = "isUnderlined"
}

type InlineStyleType = Record<InlineStyle, boolean>

enum ListStyle {
  "UL",
  "OL"
}

type LS keyof typeof ListStyle

interface HTMLBlock extends InlineStyleType {
  // This has extended with
  // isBold: boolean
  // isItalic: boolean
  // isUnderlined: boolean

  listType: LS
}
Alex Dunlop
  • 1,383
  • 15
  • 24
9

With ts-enum-util (npm, github), it's easy, type-safe (uses generics), and takes care of skipping the numeric reverse lookup entries for you:

import { $enum } from "ts-enum-util";

enum MyEnum { Hello, Goodbye };

$enum(MyEnum).map(v => {
    if (v === MyEnum.Hello) {
        return ':)';
    } else if (v === MyEnum.Goodbye) {
        return ':(';
    }
}); // produces [':(', ':)']

NOTE: ts-enum-util always iterates based on the order of the sorted enum keys to guarantee consistent order in all environments. Object.keys() does not have a guaranteed order, so it's impossible to iterate enums "in the order they were defined" in a cross-platform guaranteed way. (update: new version of ts-enum-util now preserves the original order in which the enum was defined)

If you are using string enums, then combine it with ts-string-visitor (npm, github) for even more generic type-safe compiler checks to guarantee that you handle all possible enum values in your map function: (update: new version of ts-enum-util now includes functionality of ts-string-visitor, and it works on numeric enums now too!)

import { $enum } from "ts-enum-util";
import { mapString } from "ts-string-visitor";

enum MyEnum { Hello = "HELLO", Goodbye = "GOODBYE" };

$enum(MyEnum).map(v => {
    // compiler error if you forget to handle a value, or if you
    // refactor the enum to have different values, etc.
    return mapString(v).with({
        [MyEnum.Hello]: ':)',
        [MyEnum.Goodby]: ':('
    });
}); // produces [':(', ':)']
Jeff Lau
  • 433
  • 4
  • 4
3

Maybe this will help you:

enum NumericEnums {
  'PARAM1' = 1,
  'PARAM2',
  'PARAM3',
}
enum HeterogeneousEnums {
  PARAM1 = 'First',
  PARAM2 = 'Second',
  PARAM3 = 3,
}

type EnumType = { [key: string]: string | number };
type EnumAsArrayType = {
  key: string;
  value: string | number;
}[];
const enumToArray = (data: EnumType): EnumAsArrayType =>
  Object.keys(data)
    .filter((key) => Number.isNaN(+key))
    .map((key: string) => ({
      key,
      value: data[key],
    }));

console.log(enumToArray(NumericEnums));
console.log(enumToArray(HeterogeneousEnums));

// Usage
enumToArray(HeterogeneousEnums).map(({ key, value }) => {
  console.log(`${key}: ${value}`);
  // Your necessary logic
  return null;
});

Console result

pioe
  • 31
  • 2
  • 2
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Mar 17 '22 at 18:30
2

I would not call it general but I use this many times and may it will be handy for others too:

type TMyEnum = ':)'|':(';
class MyEnum {
    static Hello: TMyEnum = ':)';
    static Goodbye: TMyEnum = ':(';
}
console.log(MyEnum.Hello); // :)
console.log(MyEnum.Goodbye); // :(

Now you don't need any mapping function and it works as expected however you have to create separate similar class for every enum (which should not be a problem since you would do at anyway). The only drawback I can think now is that you can not iterate over it's properties. But until now it wasn't a problem for me I didn't need it. And you can add a static array to the class when you need it.

Manuel Fodor
  • 464
  • 4
  • 6
0

This is a working function you can use. Below I'm passing ItemMaterial to getEnumKeys function and getting ["YELLOW", "WHITE", "ROSE", "BLACK"].

Similarly use the getEnumValues function to get values of the enum.

Take a look at the splitEnumKeysAndValues function to see how these variables extracted from the enum.

enum ItemMaterial {
  YELLOW,
  WHITE,
  ROSE,
  BLACK,
}


const keys = getEnumKeys<typeof ItemMaterial>(ItemMaterial)
const values = getEnumValues<typeof ItemMaterial, `${ItemMaterial}`>(ItemMaterial);

function getEnumKeys<TypeofEnum>(value: TypeofEnum): keyof TypeofEnum  {
    const { values, keys } = splitEnumKeysAndValues(value);

    return keys as unknown as keyof TypeofEnum;
}

function getEnumValues<TypeofEnum, PossibleValues>(value: TypeofEnum): PossibleValues[]  {
    const { values, keys } = splitEnumKeysAndValues(value);

    return values as unknown as PossibleValues[];
}

function splitEnumKeysAndValues<T>(value: T): { keys: keyof T, values: Array<string | number> } {
  const enumKeys = Object.keys(value);
 
  const indexToSplit = enumKeys.length / 2
  const enumKeysKeyNames = enumKeys.slice(0, indexToSplit) as unknown as keyof T;
  const enumKeysKeyValues = enumKeys.slice(indexToSplit);

  return {
    keys: enumKeysKeyNames,
    values: enumKeysKeyValues,
  }
}
0

An easier and shorter way to map over ENUM is using the for..in loop with whatever approach you're using for declaring enums:

const enum YOURENUM = { 'aaaa', 'bbbb', 'cccc'}
//or
const enum MYENUM = { aaaa = 'aaaa', bbbb = 'bbbb', cccc = 'cccc' }

const arr: string[] = []
for (let i in MYENUM) {
    arr.push(i.toString());
}

console.log(arr) //['aaaa', 'bbbb', 'cccc']
Anis
  • 77
  • 2
  • 7