79

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.

1

8 Answers 8

92

To map an enum do this:

(Object.keys(MyEnum) as Array<keyof typeof MyEnum>).map((key) => {})
Sign up to request clarification or add additional context in comments.

3 Comments

Adding to the above, if you want the keys as an array, use this : ``` Object.keys(MyEnum).filter((el) => { return isNaN(Number(el)) }) ```
typing is better with: (Object.keys(MyEnum) as Array< MyEnum >
This doesn't work. It produces the enum values twice: once as numbers and again as strings.
21

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.

4 Comments

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, however, it looks like using generics with enums isn't completely possible with typescript as of yet.
Which type information would you like, specifically, and where?
This function signature is an improvement: function mapEnum<T>(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 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.
17

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
}

Comments

10

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 [':(', ':)']

Comments

4

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

1 Comment

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.
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.

Comments

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,
  }
}

Comments

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']

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.