TypeScript enums offer a strong method to group and name associated constants in your applications.
Hardcoding user roles, API statuses, or application states can make your code harder to maintain and more prone to errors, especially when developing larger applications. A single typing mistake or minor alteration in your programming code can disrupt all the application’s functions.
TypeScript enums enable you to give clear and organized names to numerical values. You can define strings and numbers only once, then refer to them everywhere in the code.
This guide explains how enums work, the different types available, and when to use them. It also covers their limitations and when union types or object literals might be the better choice.
An enum is a special class in TypeScript that allows a developer to define a set of named constants. Since some values in a TypeScript project never change — like user roles or API statuses — storing them as plain strings or numbers can lead to typos and incorrect values.
Rather than using scattered raw values, enums let you define them once and reference them across your code.
For example, when implementing user authentication, an enum can be used to define different user roles:
enum UserRole { Admin = "ADMIN", Editor = "EDITOR", Viewer = "VIEWER" }
Instead of hardcoding role strings throughout your application, you can reference UserRole.Admin
, ensuring consistency and reducing errors.
Similarly, when handling an API response in a REST API, enums can represent different status codes for each response:
enum ApiStatus { Success = 200, NotFound = 404, ServerError = 500 }
This makes your code more readable and maintainable by replacing magic numbers with meaningful names.
Unlike regular objects, enums come with type safety. TypeScript prevents assigning values that do not exist in the enum. Here is an example of an enum enforcing correct values:
enum UserRole { Admin = "ADMIN", User = "USER", Guest = "GUEST" } let role: UserRole = UserRole.Admin; // Correct role = "SuperAdmin"; // Type error >
With an object, TypeScript does not stop incorrect values. This means you could mistakenly assign a value that was not originally defined:
const UserRole = { Admin: "ADMIN", User: "USER", Guest: "GUEST" }; let role = UserRole.Admin; // No issue role = "SuperAdmin"; // No Type error
Enums work best when handling fixed sets of values, such as:
Admin
, User
, Guest
Success
, Error
, Pending
Processing
, Shipped
, Delivered
Mon
, Tues
, etc.Next, we’ll look at the different types of enums and how they work.
Enums in TypeScript can store values as numbers, strings, or a mix of both. Each type works differently and is suited for specific use cases.
By default, TypeScript assigns numbers to enum members, starting from 0
. If you do not specify values, TypeScript increments them automatically. Here is an example of a numeric enum with default values:
enum Direction { Up, // 0 Down, // 1 Left, // 2 Right // 3 }
If you need specific values, you can manually assign them. TypeScript will continue counting from the last assigned number:
enum Status { Success = 1, Failure, // 2 Pending // 3 }
One unique feature of numeric enums is reverse mapping, which allows you to retrieve the key using its value. This can be useful but may also expose values unexpectedly:
console.log(Status.Success); // 1 console.log(Status[1]); // "Success"
String enums require manually assigned values, making them easier to read. They also prevent unexpected assignments since TypeScript enforces exact matches. Here is how a string enum is defined:
enum Status { Success = "SUCCESS", Failure = "FAILURE", Pending = "PENDING" }
Unlike numeric enums, they do not allow reverse mapping, but they make comparisons clearer. For example, you can check an API response like this:
function checkStatus(status: Status) { if (status === Status.Success) { console.log("Everything is good"); } }
TypeScript allows enums that mix numbers and strings, but this is rarely a good idea. It makes checking values harder and can introduce unnecessary complexity. Here is an example of a heterogeneous enum:
enum Mixed { Yes = "YES", No = 0 }
Since combining types makes code harder to read and debug, it is best to avoid heterogeneous enums unless absolutely necessary.
Next, we’ll look at key features of enums and how they affect performance.
TypeScript enums come with a few built-in behaviors that set them apart from regular objects. Some of these can be useful, while others might cause unexpected issues if not understood properly.
Numeric enums allow reverse mapping, meaning you can look up both the value from the key and the key from the value. Here is how it works:
enum Status { Success = 1, Failure = 2, Pending = 3 } console.log(Status.Success); // 1 console.log(Status[1]); // "Success"
This can help with debugging, but also makes all enum values visible at runtime, which may not be ideal for sensitive data. String enums do not support reverse mapping, so looking up a key from a value will return undefined
:
enum Status { Success = "SUCCESS", Failure = "FAILURE" } console.log(Status["SUCCESS"]); // undefined
Enum values can be constant (fixed at compile time) or computed (determined at runtime). Here is an example where some values are set dynamically:
enum MathValues { Pi = 3.14, // Constant Random = Math.random(), // Computed Length = "Hello".length // Computed }
If an enum has a computed member, TypeScript requires any members after it to have explicit values. Here is an example of this rule in action:
enum Example { First = 1, Second = Math.random(), // Computed Third = 3 // This must be explicitly assigned }
const
enums and reducing runtime overheadBy default, enums generate JavaScript objects, increasing file size. const
enums prevent this by replacing enum references with their values at compile time. Here is how a const
enum
is defined:
const enum Direction { Up, Down, Left, Right } let move = Direction.Up;
When compiled, this becomes:
let move = 0;
Since const
enums do not create objects, they cannot be used for runtime operations like key lookups.
Next, we’ll look at how enums can be used in real-world applications.
Typescript enums help keep fixed values organized and consistent across an application. They are useful for managing user roles, API responses, and application states without relying on raw strings or numbers.
User roles often define what a person can or cannot do in an application. Instead of checking raw strings, enums make these roles clear and prevent mistakes. Here is how user roles can be structured using an enum:
enum UserRole { Admin = "ADMIN", User = "USER", Guest = "GUEST" } function checkAccess(role: UserRole) { if (role === UserRole.Admin) { console.log("Access granted: Full permissions"); } else if (role === UserRole.User) { console.log("Access granted: Limited permissions"); } else { console.log("Access denied"); } } checkAccess(UserRole.Admin); // "Access granted: Full permissions"
Since the roles are predefined, there is no risk of typos like “admin”
instead of “ADMIN”
.
APIs return status codes to indicate whether a request was successful or failed. Using an enum makes it clear what each status represents. Here is how an API response status can be defined:
enum ApiResponseStatus { Success = 200, NotFound = 404, ServerError = 500 } function handleResponse(status: ApiResponseStatus) { if (status === ApiResponseStatus.Success) { console.log("Request was successful"); } else if (status === ApiResponseStatus.NotFound) { console.log("Resource not found"); } else { console.log("Something went wrong on the server"); } } handleResponse(ApiResponseStatus.Success); // "Request was successful"
Instead of remembering that 200
means “success” or 404
means “not found,” the enum makes these codes readable and easy to update in one place if needed.
Applications often switch between different states, such as loading, ready, or error. Using an enum keeps these states well-defined and prevents unexpected values. Here is how an enum can represent application states:
enum AppState { Loading, Loaded, Error } let currentState: AppState = AppState.Loading; function updateState(state: AppState) { if (state === AppState.Loading) { console.log("App is loading..."); } else if (state === AppState.Loaded) { console.log("App is ready"); } else { console.log("Something went wrong"); } } updateState(currentState); // "App is loading..." currentState = AppState.Loaded; updateState(currentState); // "App is ready"
This approach keeps state management structured, prevents invalid values, and makes the code easier to maintain.
Next, we’ll look at some of the limitations of enums and when they might not be the best choice.
Enums are useful, but they come with some trade-offs. They add extra JavaScript code, can expose internal values, and may not always work well with APIs. These issues can affect performance, security, and type safety.
Unlike other TypeScript features that disappear after compilation, enums turn into JavaScript objects. This increases file size and runtime overhead.
Here is a simple enum in TypeScript:
enum Direction { Up, Down, Left, Right }
After compilation, this turns into:
var Direction; (function (Direction) { Direction[(Direction["Up"] = 0)] = "Up"; Direction[(Direction["Down"] = 1)] = "Down"; Direction[(Direction["Left"] = 2)] = "Left"; Direction[(Direction["Right"] = 3)] = "Right"; })(Direction || (Direction = {}));
For large projects, this extra code adds up. To avoid this, const enum
removes the object entirely.
Here is how a const
enum
works:
const enum Direction { Up, Down, Left, Right } let move = Direction.Up;
By using const enum
, the compiled output is much cleaner and more efficient. However, this approach has limitations, such as not being able to dynamically reference enum values at runtime.
Numeric enums allow reverse mapping, meaning their values can be accessed both ways. This can expose sensitive values in front-end applications. Here is an example:
enum UserRole { Admin = 1, User = 2, Guest = 3 } console.log(UserRole[1]); // "Admin"
An attacker inspecting the JavaScript output could list all roles. String enums do not allow reverse mapping, making them a safer choice.
While enums prevent typos, numeric enums allow any number to be assigned, even if it is not defined. Here is what that looks like:
enum Direction { Up, Down } let move: Direction = 10; // No TypeScript error
This defeats the purpose of type safety. String enums do not have this issue, as only valid values can be assigned.
Next, we’ll look at better alternatives to enums and when to use them.
While enums help organize constants, union types, object literals, and const
enums can sometimes be a better choice.
Union types enforce fixed values without adding extra JavaScript.
Here is how they work:
type Status = "Success" | "Failure" | "Pending"; let response: Status = "Success"; // Allowed response = "Error"; // Type error
This example shows how union types restrict response
to only the specified values, catching invalid assignments at compile time.
Object literals act like enums but allow dynamic behavior and better optimization. Here is an example:
const Status = { Success: "SUCCESS", Failure: "FAILURE", Pending: "PENDING" } as const; let response = Status.Success; // Works response = "Error"; // Type error
This demonstrates how object literals with a const
create immutable, type-safe constants that can be used like enums.
const
enums for performanceconst
enums remove the object wrapper and inline values directly, reducing file size. Here is how they work:
const enum Direction { Up, Down, Left, Right } let move = Direction.Up; // Compiles to: let move = 0;
This example highlights how const enum
eliminates runtime objects by replacing enum references with their literal values during compilation.
When working with TypeScript enums, following best practices makes your code type safe and easy to maintain. It also improves the performance of your applications. Here are five key best practices to follow:
String enums reduce errors by deterring accidental assignments and making problems easier to debug. They provide more readable and predictable values compared to numeric enums.
const
enum
when you want to optimize application performanceIf an enum’s values do not need to be checked at runtime, use const enum
to reduce compiled JavaScript size. This approach inlines enum values directly, eliminating runtime overhead.
Putting string and number values in an enum makes it harder to understand and can produce unexpected results. Stick to homogeneous enums (all strings or all numbers) for clarity and consistency.
When you require flexible value storage, use other methods like objects or union types instead of enums. Enums are best suited for fixed, unchanging sets of values.
For simple scenarios, union types offer improved type safety with minimal JavaScript overhead. They are a lightweight alternative to enums when dealing with a small set of fixed values.
If you need a fixed set of values that won’t change, enums help keep your code structured and prevent accidental mistakes. String enums are usually the best choice since they are readable and avoid issues like reverse mapping. When enums start to feel unnecessary, union types or object literals might be the better way to go.
Choose what works best for your project. Union types keep things simple and do not add extra JavaScript. Object literals give you more flexibility, while const
enums help reduce file size. There is no one-size-fits-all solution, so use what makes your code easier to manage.
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 nowReview the basics of react-scripts, its functionality, status in the React ecosystem, and alternatives for modern React development.
Explore the fundamental commands for deleting local and remote branches in Git, and discover more advanced branch management techniques.
AbortController
APICheck out a complete guide on how to use the AbortController and AbortSignal APIs in both your backend and frontend.
LLMs can do more than chat! Explore alternative integration models that improve efficiency, reduce complexity, and enhance user control.