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:
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.
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:
0
(or with the number we specify, 1
, in our example). This is represented by the first three lines in the output abovestring
value is assigned a numeric key; this is a reverse mappingIf 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.
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 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
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.
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.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.