Coastal Media Brand

Enums are ubiquitous in modern programming. They are normally widely used to model categories of things, such as the possible states of a traffic light, the days of the week, and the months of the year.

One of the advantages of enums is that they let us map short lists of values into numbers, making it easier to compare them and work with them in general. Enums have evolved a lot over the years, from simple mapping from “words” to numbers to complex data structures similar to classes, with methods and parameters.

In this article, we’ll explore different approaches for iterating over enums in TypeScript.

Jump ahead:

Why iterate in TypeScript

TypeScript enums are quite simple objects:

enum TrafficLight {
  Green = 1,
  Yellow,
  Red
}

In the definition above, Green is mapped to the number 1. The subsequent members are mapped to auto-incremented integers. Hence, Yellow is mapped to 2, and Red to 3.

If we didn’t specify the mapping Green = 1, TypeScript would pick 0 as a starting index.

However, sometimes we want to iterate over a TypeScript enum. This is particularly useful, for example, when we want to perform some actions for each element of an enumeration.

In TypeScript, there are a few ways to iterate over enums. Let’s take a look.

Inbuilt object methods

The simplest way to iterate over an enum in TypeScript is using the inbuilt Object.keys() and Object.values() methods. The former returns an array containing the keys of the objects, whereas the latter returns the values.
The following snippet of code shows how to use an inbuilt objects method to list the keys and values of an enum:

const keys = Object.keys(TrafficLight)
keys.forEach((key, index) => {
        console.log(`${key} has index ${index}`)
})

The example above prints the following:

"1 has index 0"
"2 has index 1"
"3 has index 2"
"Green has index 3"
"Yellow has index 4"
"Red has index 5"

The first three lines may look confusing. Why do we have three unexpected keys? This is because enums are compiled in two ways:

  • each element is assigned to a numeric value starting with 0 (or with the number we specify, 1, in our example). This is represented by the first three lines in the output above
  • each string value is assigned a numeric key; this is a reverse mapping

If we want to only list the string keys, we’ll have to filter out the numeric ones:

const stringKeys = Object
    .keys(TrafficLight)
    .filter((v) => isNaN(Number(v)))
stringKeys.forEach((key, index) => {
    console.log(`${key} has index ${index}`)
})

In this case, the snippet above prints the following:

"Green has index 0"
"Yellow has index 1"
"Red has index 2"

From the output above, we can see that the index parameter has nothing to do with the actual numeric value in the enum. In fact, it is just the index of the key in the array returned by Object.keys().

Similarly, we can iterate over the enum values:

const values = Object.values(TrafficLight)
values.forEach((value) => {
    console.log(value)
})

Again, the snippet above prints both string and numeric values:

"Green"
 "Yellow"
 "Red"
1
2
3

Let’s say we’re interested in the numeric values, not in the string ones. We can filter the latter out similarly as before, using .filter((v) => !isNaN(Number(v))).

It’s worth noting that we have to filter the values only because we’re dealing with numeric enums. If we had assigned a string value to the members of our enumeration, we wouldn’t have to filter out numeric keys and values:

enum TrafficLight {
  Green  = "G",
  Yellow = "Y",
  Red        = "R"
}
Object.keys(TrafficLight).forEach((key, index) => {
    console.log(`${key} has index ${index}`)
})
Object.values(TrafficLight).forEach((value) => {
        console.log(value)
})

The snippet above prints what follows, where the first three lines are from the first forEach loop and the last three lines are from the second forEach loop:

"Green has index 0"
"Yellow has index 1"
"Red has index 2"
"G"
"Y"
"R"

String enums are very useful, as they are more human-readable than numeric ones. We can also mix numeric and string enums, although it is not advisable to do so.

Using the Object.keys() and Object.values() methods to iterate over the members of enums is a quite simple solution. Nonetheless, it is also not very type-safe, as TypeScript returns keys and values as strings or numbers, thus not preserving the enum typing.

For loops

Instead of relying on Object.keys() and Object.values(), another approach is to use for loops to iterate over the keys and then use reverse mapping to get the enum values:

enum TrafficLight {
        Green,
        Yellow,
        Red
}
for (const tl in TrafficLight) {
        const value = TrafficLight[tl]
        if (typeof value === "string") {
                        console.log(`Value: ${TrafficLight[tl]}`)
        }
}

The script above will print the following:

"Value: Green"
"Value: Yellow"
"Value: Red"

You’ll notice that in the example above we filtered out the numeric values. This way, we can extract the member names of our enum. If we wanted to fetch them, instead of the string values, we could use a different guard in the if statement: typeof value !== "string".

The code above won’t work if the enum is backed by string values. In particular, the compilation will fail with the following error message: Element implicitly has an 'any' type because an expression of type 'string' can't be used to index type 'typeof TrafficLight'. No index signature with a parameter of type 'string' was found on type 'typeof TrafficLight'<./p>

This is because the type of value is now string and TrafficLight[] only accepts either Green, Yellow, or Red.

To fix this, we’ll have to explicitly inform TypeScript about the type of our keys:

enum TrafficLight {
        Green  = "G",
        Yellow = "Y",
        Red         = "R"
}
function enumKeys<O extends object, K extends keyof O = keyof O>(obj: O): K[] {
        return Object.keys(obj).filter(k => !Number.isNaN(k)) as K[]
}
for (const tl of enumKeys(TrafficLight)) {
        const value = TrafficLight[tl]
        if (typeof value === "string") {
                        console.log(`Value: ${TrafficLight[tl]}`)
        }
}

There are two main novelties in the example above. First, the enumKeys function simply extracts the keys of the enum and returns them as an array. In the example above, the return type of enumKeys(TrafficLight) is ("Green" | "Yellow" | "Red")[]. Secondly, we use for…of, rather than for…in, in our for loop. The main difference is that the latter returns the values of the array, that is Green, Yellow, and Red. The former, on the other hand, returns their string representation.

If we run the example above, we’ll get the following output, as expected:

"Value: G"
"Value: Y"
"Value: R"

The benefit of this latter approach is that the type of the enum value is preserved. In particular, value has type TrafficLight in the for loop, rather than string or number.

Lodash

Lodash is a JavaScript library that provides many utility methods for common programming tasks. Such methods use the functional programming paradigm to let us write code that’s more concise and readable.

To install Lodash into our project, we can run the following command:

npm install lodash --save

The npm install lodash command will install the module, and the save flag will update the contents of the package.json file.

It turns out we can leverage its forIn method to iterate over an enum in TypeScript:

import { forIn } from 'lodash'
enum TrafficLight {
      Green = 1,
      Yellow,
      Red,
}
forIn(TrafficLight, (value, key) => console.log(key, value))

The forIn method iterates over both keys and values of a given object, invoking, in its simplest form, a given function for each (key, value) pair. It is essentially a combination of the Object.keys() and Object.values() methods.

As we might expect, if we run the example above, we’ll get both string and numeric keys:

1 Green
2 Yellow
3 Red
Green 1
Yellow 2
Red 3

As before, we can easily filter out string or numeric keys, depending on our needs:

import { forIn } from 'lodash'
enum TrafficLight {
        Green = 1,
        Yellow,
        Red,
}
forIn(TrafficLight, (value, key) => {
        if (isNaN(Number(key))) {
                  console.log(key, value)
        }
})

The example above prints the following:

Green 1
Yellow 2
Red 3

In this case, the type of key is string, whereas the type of value is TrafficLight. Hence, this solution preserves the typing of the value:

import { forIn } from 'lodash'
enum TrafficLight {
    Green  = "G",
    Yellow = "Y",
    Red     = "R"
}
forIn(TrafficLight, (value, key) => {
    if (isNaN(Number(key))) {
              console.log(key, value)
    }
})

As we might expect, the example above prints the following:

Green G
Yellow Y
Red R

Conclusion

In this article, we reviewed a few ways to iterate over the keys and values of enum types in TypeScript. First, we used the inbuilt methods of any TypeScript object, noting that they are fairly “low-level”.

Second, we moved to a higher-level approach, with for loops. We verified that we can teach TypeScript to preserve the typing given by enums, without relying on the string or numeric representation. Third, we considered a convenient method of the Lodash library, forIn.

For each of the approaches we considered, we analyzed the differences between numeric and string-backed enums. As usual, there’s no unique “right” solution. The way you iterate over the key/values of your enums strongly depends on what you have to do and whether you wish to preserve the enum typing.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.

Coastal Media Brand

© 2024 Coastal Media Brand. All rights Reserved.