Editor’s note: This article was last updated by Pascal Akunne on 30 July 2024 to include information on converting enums to string values and vice versa and explore the scenarios where this would be useful, and discuss advanced patterns for string enums, such as using enum values as functions or in API contract definitions.
In this article, we will explore string enums in TypeScript. If you are new to the JavaScript/TypeScript landscape, you might be wondering what enums are. The enums
keyword offers a way for us to define a finite set of values — usually as named constants in a strongly typed way. They also allow us to specify a list of values for a particular collection set or type.
Enums are not natively supported in JavaScript, however, Object.freeze
can be used to imitate their functionality. This is because TypeScript treats enums as if they were real objects at runtime, even non-const enums. We can use this construct as shown in the example below:
const directionEnum = Object.freeze({ UP : "UP", DOWN: "DOWN" }); console.log(directionEnum) //{ UP: 'UP', DOWN: 'DOWN' }
By doing this, we get an object that is read-only and protected. Note that the read-only definition is only shown when we hover the mouse over the directionEnum
const:
const directionEnum: Readonly<{ UP: string; DOWN: string; }>
The Object.freeze
method prevents the modification of existing property attributes and values, as well as the addition of new properties. This closely mirrors the idea behind enums because they are meant to have a definite number of fixed values for a given variable declaration.
Enums are not a new concept in programming. As you may already know, most programming languages like Java, C, and so on, have the concept of enums defined in their core. We can also picture enums as artificially created types that contain a finite set of values, just like we have the Boolean type, which contains only false
and true
values.
As a result of their usefulness and advantages in other languages, enums were introduced and added to the TypeScript landscape. However, they are unique and specific to TypeScript in that they are not a typed extension of, equivalent to, or correspond with a specific feature in the JavaScript language, despite TypeScript being a typed superset of JavaScript.
Note: There is a current ECMAScript stage-0 proposal to add enums to JavaScript.
Enums are number-based by default. For number-based enums, members are distinct from each other due to their auto-incrementing nature. They can either have an initializer (whereby we explicitly specify the enum member values) or not.
Let’s see an example below:
enum statusEnumWithInitializer = { "OPEN" = 10, "CLOSE", } //statusEnumWithInitializer.CLOSE = 11 //Since the following members are auto incremented from that point on.
If we leave off the initializers, we have this:
enum statusEnumWithoutInitializer = { "OPEN", "CLOSE", } //statusEnumWithoutInitializer.OPEN = 0 //statusEnumWithoutInitializer.CLOSE = 1
As we can see from the above examples, accessing the members of an enum is as simple as accessing the properties of objects. We mentioned earlier that the reason for this is that non-const enums have object-like behavior.
Notice the trailing commas in the above code snippet. Using these enum types in real-world cases is quite straightforward because we can simply declare them as types and pass them as arguments to functions.
Because number-based enums automatically increment their values, there’s a potential issue when enum members don’t have explicit initial values. They must either be declared first or follow numeric enum members that are initialized with number-based constants. This rule also applies to mixed (heterogeneous) enums. This is because enums need to be fully evaluated at compile time:
// Correctly initialized number-based enum enum Color { Purple, // 0 Red, // 1 Black, // 2 } // Enum with explicit initializer enum Month { Jan = 1, Feb, // 2 Mar, // 3 } // Heterogeneous enum with explicit initializer enum Mixed { Fan = "Fan", Chair = "Chair", Table = 1, Bed, // 2 (auto-incremented from 1) } // Incorrect enum declaration enum InvalidEnum { Top = "Top", Center, // Error: Cannot infer type from previous member Bottom = 1, }
Usually, enum types come in handy when we intend to declare types that must satisfy certain criteria defined in the enum declarations. As we mentioned earlier, while enums are numerically based by default, TypeScript ≥ version 2.4 supports string-based enums.
String-based enums, just like object literals, support computed names with the use of the square bracket notation, and this is usually not the case for number-based enums. Therefore, this limits our ability to use number-based enums in JSON objects, as it is usually not possible to compute the names of these kinds of enum members.
String enums are serializable over transfer protocols and are easily debuggable — they are just strings, after all. They also allow for a meaningful and readable value at runtime, independent of the name of the enum member.
For completely string-based enums, we cannot omit any initializers, as all enum members must come with values. But this is not the case for numeric enums, which end up as only plain numbers and therefore might not be very useful.
Throughout this article, we will focus on string-based enums. Please refer to the TypeScript documentation for more information on number-based enums. If you need a general introduction to enum types or an overview of ways enums could be misused in the language, the LogRocket blog has you covered.
Constant-based enums are enums that have a single member without an initializer value. This means that they are automatically assigned the value of 0
.
They can also have more than one member value, whereby the first member must be a numeric constant. This means that subsequent values are incremented by adding one to the preceding numeric constants, in that order.
In summary, for constant enums, the enum member value can be the first member without an initializer or must have an initializer that is numeric, if it has other enum members preceding it.
Note that constant enum values can be computed or calculated at compile time. In the case of computed enums, they can be initialized via expressions. See an example of a computed enum below:
enum computedEnum { a = 10 str = "str".length // computed enum add = 300 + 100 //computer enum }
We can specify enum member values as needed in the following ways:
In general, literal types are JavaScript primitive values. As of TypeScript ≥ version 1.8, we can create string literal types. Specifically, string literal types allow us to define a type that accepts only one specific string literal. On their own, they are usually not very useful, but when they are combined with union types, they become immensely powerful.
String literal types mimic a string enum’s expected behavior when used in conjunction with union types, as they also provide a reliable and safe experience for named string values. See the example below:
type TimeDurations = 'hour' | 'day' | 'week' | 'month'; var time: TimeDurations; time = "hour"; // valid time = "day"; // valid time = "dgdf"; // errors
From the code above, the TimeDurations
type looks like a string enum, in that it defines several string constants.
Enum types can effectively become a union type of each enum member. A combination of string literals and union types offers as much safety as enums and has the advantage of translating more directly to JavaScript. It also offers similarly strong autocomplete in various IDEs. If you’re curious, you can quickly check this on the TypeScript Playground.
For a string enum, we can produce a union type out of the literal types of its values, but it doesn’t happen the other way.
String-based enums were only introduced to TypeScript in version 2.4, and they made it possible to assign string values to an enum member. Let’s look at an example from the documentation below:
enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT", }
Before now, developers had to rely on using string literals and union types to describe a finite set of string values, just as string-based enums do. So, for example, we could have a type defined like so:
type Direction = "UP" | "DOWN" | "LEFT" | "RIGHT";
We can then make use of the type as, say, a function parameter, and the language can check that these exact types are passed at compile time to the function when instantiated.
In summary, to make use of string-based enum types, we can reference them by using the name of the enum and their corresponding value, just as you would access the properties of an object.
At runtime, string-based enums behave just like objects and can easily be passed to functions like regular objects. See the TypeScript documentation for an example of how enums behave as objects at runtime.
Remember that for string-based enums, the initializers cannot be omitted, unlike for number-based enums, where the initializer provided is either the first member or the preceding member and must have a numeric value. Each enum member must be initialized with a constant that is either a string literal or another enum member that is a string and part of a string enum.
String enums are heavily used in JSON objects for validating API calls to do things like ensure parameters are passed correctly. Another wonderful use case is in their application for defining domain-specific values for predefined APIs.
The importance of enum types can’t be overstated. For instance, whenever we make use of an enum member in our code for validation purposes, TypeScript checks statically that no other values are used.
They also come in handy for ensuring safe string constants. Enums offer a more self-descriptive option than making use of Boolean values. Instead, we can specify enums that are unique to that domain, which makes our code more descriptive.
Therefore, we can decide to add more options later, if we need to, as compared to when we use Boolean checks. Let’s explore an example below where we can check if an operation succeeded or failed via a Boolean check and with the use of an enum type declaration:
class Result { success: boolean; // in our code we can set this to either true or false // also we must have seen constructs like `isSuccess` = true or false } //compared to using enums which are more descriptive of our intentions enum ResultStatus { FAILURE, SUCCESS } class enumResult { status: ResultStatus; }
To create a type whose values are the keys of enum members, we can make use of keyof
with the typeof
method:
enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT", } // this is same as //type direction = 'Up' | 'Down' | 'Left' | 'Right'; type direction = keyof typeof Direction;
As the TypeScript documentation says, even though enums are real objects that exist at runtime, the keyof
keyword works differently than you might expect for typical objects. Instead, using keyof typeof
will get you a type that represents all enum keys as strings, as we have seen above.
A subtle difference between number-based enums and string-based enums is that number-based enums have reverse mapping for number-valued members.
Reverse mapping allows us to check if a given value is valid in the context of the enum. To better understand this, let us look at the compiled output of a number-based enum:
//As we can see for number-based Enums, we could decide to leave off the initializers and the members autoincrements from Unauthorized -401 and so on - enum StatusCodes { OK = 200, BadRequest = 400, Unauthorized, Forbidden, NotFound, } //the transpiled JavaScript file is shown below (ignore the wrong status code ;)) var StatusCodes; (function (StatusCodes) { StatusCodes[StatusCodes["OK"] = 200] = "OK"; StatusCodes[StatusCodes["BadRequest"] = 400] = "BadRequest"; StatusCodes[StatusCodes["Unauthorized"] = 401] = "Unauthorized"; StatusCodes[StatusCodes["Forbidden"] = 402] = "Forbidden"; StatusCodes[StatusCodes["NotFound"] = 403] = "NotFound"; })(StatusCodes || (StatusCodes = {}));
If we look at the code above, we see that we can resolve a value by its key, and a key by its value. For example, StatusCodes["OK"] = 200 is also equal to StatusCodes[200] = "OK"
.
Unfortunately, this does not apply to string-based enums. Instead, in string-based enums, we can assign string values directly to enum members. Let us see the transpiled output below:
enum Direction { Up = "UP", Down = "DOWN", Left = "LEFT", Right = "RIGHT", } //the transpiled .js file is shown below - var Direction; (function (Direction) { Direction["Up"] = "UP"; Direction["Down"] = "DOWN"; Direction["Left"] = "LEFT"; Direction["Right"] = "RIGHT"; })(Direction || (Direction = {})); enum Direction { Up, Down, Left, Right, } //this will show direction '1' console.log(Direction.Down); // this will show message 'Down' as string representation of enum member console.log(Direction[Direction.Down]);
String enums can be easily inferred from their indices or by looking at the initializer of their first member. The auto-initialized value on an enum is its property name. To get the number value of an enum member from a string value, you can use this:
const directionStr = "Down"; // this will show direction '1' console.log(Direction[directionStr]);
We can use these values to make simple string comparisons. Here’s an example:
// Sample code to check if a user presses "UP" on say a game console const stringEntered = "UP" if (stringEntered === Direction.Up){ console.log('The user has pressed key UP'); console.log(stringEntered); //"UP" }
As mentioned earlier, logging members of numeric enums is not very useful because we are seeing only numbers. There is also the issue of loose typing when using these enum types, as the values that are allowed statically are not only those of the enum members — any number is accepted. Weird, right? Let’s see a demonstration below:
const enum LogLevel { ERROR, WARN, INFO, DEBUG, } function logger(log: LogLevel) { return 'different error types' } console.log(logger(LogLevel.ERROR)) // "different error types" console.log(logger(12)) // "different error types"
With const-based enums, we can avoid the generated code by the TypeScript compiler, which is useful when accessing enum values. Const-based enums do not have a representation at runtime. Instead, the values of its members are used directly.
Const-based enums are defined as regular enums except with the use of the const
keyword. The only difference is in their behavior and usage. Let’s see an example:
const enum test { A, B, } //usage function testConst(val: test) { if(test.A) { return "A" } if(test.B) { return "B" } else { return "Undefined" } }
Const-based enums can only use constant enum expressions and are not added during compilation, which is unlike regular enum declarations. Const enum members are also inlined at use sites. This, therefore, infers that const enums cannot have computed members.
After compilation, they are represented as shown below:
console.log(testConst) function testConst(val) { if (0 /* A */) { return "A"; } if (1 /* B */) { return "B"; } }
As we can see from above, the enum member values are inlined by TypeScript. Const enums also do not allow for a reverse lookup, which behaves like regular JavaScript objects.
Enum members can also become types. This is applicable only when the enum members are assigned their own values. We should also note that to emit the mapping code regardless of const enums, we can turn on the preserveConstEnums
compiler option in our tsconfig.json
file.
If we compile our code again with the preserveConstEnums
option set in the TypeScript config file, the compiler will still inline the code for us, but it will also emit the mapping code, which becomes useful when a piece of JavaScript code needs access to it.
In modern TypeScript, there is an approach of implementing string-based enums so that they are backward compatible in the JavaScript landscape. This is done in the hope that we can make use of this syntax when the ECMAScript finally implements enums in JavaScript. This is where legacy support comes into the picture.
To do this in TypeScript, we can use the as const
method with a regular object. Let us see an example below:
const Direction = { Up: 0, Down: 1, Left: 2, Right: 3, } as const;
However, to use this approach to get the keys in our code, we need to use the keyof typeof
method. See an example below:
type Dir = typeof Direction[keyof typeof Direction];
In TypeScript, it is common to need to convert between enums and string values, particularly when working with external data sources like databases or APIs, where data is usually serialized as strings.
Because string enums naturally map to string values, converting an enum to a string is simple:
enum Status { Processing = "PROCESSIING", Complete = "COMPLETE", Suspend = "SUSPEND" } // Convert enums to string const currentStatus: Status = Status.Processing; const statusString: string = currentStatus; console.log(statusString); // Outputs: PROCESSING
Usually, we compare a string against the possible enum values to convert it back to an enum value. One way to accomplish this is by directly checking the enum values or using a type guard method:
function isValidStatus(status: string): status is Status { return Object.values(Status).includes(status as Status); } function getStatusFromString(statusString: string): Status | undefined { if (isValidStatus(statusString)) { return statusString as Status; } return undefined; // Return undefined if the string is not a valid enum } const statusFromAPI = "SUSPEND"; const statusEnum = getStatusFromString(statusFromAPI); if (statusEnum !== undefined) { console.log(`Retrieved enum: ${statusEnum}`); // Outputs: Retrieved enum: SUSPEND } else { console.log("Invalid status string"); }
TypeScript enums allow us to perform reverse lookup operations with numeric enums, but for string enums, you manually ensure string mapping:
const statusString = "Complete"; const statusEnum = Status[statusString as keyof typeof Status]; // keyof typeof is used to access enum keys if (statusEnum) { console.log(`${statusEnum}`); // Outputs: COMPLETE } else { console.log("Invalid status string."); }
When interacting with REST APIs or GraphQL endpoints, data is typically transmitted as JSON strings. Managing request and response data necessitates converting between strings and string enums. An API may, for example, return a status string that needs to be mapped to an enum in order to be further processed.
When reading from or writing to databases, string values are often used to represent categorical data. In order to guarantee type safety in your application logic, these strings can be converted to enums.
Assigning a function to each member of an enum is a complicated pattern. By using this method, we can encapsulate behavior in our enums, making them more expressive:
enum Operation { Add = "ADD", Subtract = "SUBTRACT", Multiply = "MULTIPLY", Divide = "DIVIDE", } // Map each enum member to a function const operationFunctions: { [key in Operation]: (a: number, b: number) => number } = { [Operation.Add]: (a, b) => a + b, [Operation.Multiply]: (a, b) => a * b, [Operation.Divide]: (a, b) => a / b, [Operation.Subtract]: (a, b) => a - b, }; // Using the functions function performOperation(op: Operation, a: number, b: number): number { return operationFunctions\[op\](a, b); } console.log(performOperation(Operation.Subtract, 32, 18)); // Outputs: 14 console.log(performOperation(Operation.Multiply, 44, 50)); // Outputs: 2200
We can give enum members extra functionality or metadata by combining enums with namespaces. Together with the enum specification, this pattern aids in organizing necessary logic and data:
enum NotificationType { Success = "SUCCESS", Warning = "WARNING", Error = "ERROR", } namespace NotificationType { export function getColor(type: NotificationType): string { switch (type) { case NotificationType.Success: return "green"; case NotificationType.Warning: return "yellow"; case NotificationType.Error: return "red"; default: return "gray"; } } } // Using the namespace functions const type: NotificationType = NotificationType.Error; console.log(NotificationType.getColor(type)); // Outputs: red
In situations when there are numerous lists of comparable enum members, auto-generating enum values can make maintenance easier and minimize errors. This can be achieved using TypeScript’s features or build-time scripts.
Consider an enum for HTTP methods:
// HTTP methods const httpMethods = ["GET", "POST", "PUT", "DELETE"] as const; // Auto-generate the enum values using a mapped type enum HttpMethodEnum { Get = "GET", Post = "POST", Put = "PUT", Delete = "DELETE", } function handleRequest(method: HttpMethodEnum): void { switch (method) { case HttpMethodEnum.Get: console.log("Handling GET request"); break; case HttpMethodEnum.Post: console.log("Handling POST request"); break; case HttpMethodEnum.Put: console.log("Handling PUT request"); break; case HttpMethodEnum.Delete: console.log("Handling DELETE request"); break; } } handleRequest(HttpMethodEnum.Delete); // Outputs: Handling DELETE request
Enums provide valid values for request parameters, response data, or configuration choices, making them useful for strong definitions for API contracts. This ensures type safety and consistency across our application and external services:
// Define the enum for task statuses enum TaskStatus { New = "NEW", Processing = "PROCESSING", Complete = "COMPLETE", } // API request and response types interface Task { id: number; status: TaskStatus; } // Function to update a task's status async function updateTaskStatus(taskId: number, newStatus: TaskStatus): Promise<Task> { const response = await fetch(`/api/tasks/${taskId}`, { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ status: newStatus }), }); const updatedTask: Task = await response.json(); return updatedTask; } // Using the API updateTaskStatus(1, TaskStatus.Processing) .then((task) => console.log(`Task updated: ${task.status}`)) .catch((error) => console.error("Failed to update task", error));
String enums are flexible in that we can either add key-values as enums, or just keys, if the key-values are the same and if we do not care about the case sensitivity of our enums. When using string enums in TypeScript, we do not necessarily need to know the exact strings each enum value contains.
It is also important to point out that while enums can be mixed with string and numeric members (heterogeneous), it is not clear why you would ever want to do this.
Enum member values can either be constant or computed, as we discussed earlier. For constant enums, the enum must be the first member and it has no initializer. Also, their values can be computed or calculated at compile time. The TypeScript documentation contains more details on constant versus computed enums if you’re interested.
The wonderful thing about working with a typed language like TypeScript and using a feature like this is that popular IDEs like Visual Studio Code can help us choose enum values from a list of values via autocomplete. This is especially useful when we are tasked with making comparisons between or among values from an enum, or even in their regular usage.
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.
2 Replies to "TypeScript string enums, and when and how to use them"
Great article! Helped me a lot.
I think I found one error:
// this is same as
// type direction = ‘UP’ | ‘DOWN’ | ‘LEFT’ | ‘RIGHT’;
type direction = keyof typeof Direction;
The comment should be:
// type direction = ‘Up’ | ‘Down’ | ‘Left’ | ‘Right’;
Hello Matt, yes thanks for the feedback and I’m glad you found the post helpful. We will update the comment as soon as possible. Thanks again