Stop Using TypeScript Enums: Here's Why
TypeScript enums seem convenient but come with hidden problems. Learn better alternatives that keep your code type-safe and bundle-friendly.
Stop Using TypeScript Enums: Here's Why
Enums are one of TypeScript's oldest features, borrowed from languages like C# and Java. At first glance, they seem like a convenient way to define a set of named constants. But dig deeper, and you'll find a mountain of problems that make them a poor choice for modern TypeScript projects.
In this post, I'll explain exactly why you should avoid enums and what to use instead.
What Are TypeScript Enums?
Before we tear them apart, let's understand what enums are:
enum Status { Pending, Approved, Rejected}
// Usageconst orderStatus: Status = Status.Approved;
if (orderStatus === Status.Approved) { console.log("Order approved!");}Looks clean, right? Here's where the problems begin.
---
Problem #1: Enums Generate Surprising JavaScript
Unlike most TypeScript features that simply disappear at compile time, enums generate actual JavaScript code. And it's not pretty.
enum Status { Pending, Approved, Rejected}Compiles to:
var Status;(function (Status) { Status[Status["Pending"] = 0] = "Pending"; Status[Status["Approved"] = 1] = "Approved"; Status[Status["Rejected"] = 2] = "Rejected";})(Status || (Status = {}));What just happened?
TypeScript created a bidirectional mapping object. You can look up both ways:
Status.Pending // 0Status[0] // "Pending"This reverse mapping is rarely useful but always increases your bundle size. For each enum value, you're shipping twice the code you need.
---
Problem #2: String Enums Are Only Slightly Better
"I'll just use string enums!" you might think:
enum Status { Pending = "PENDING", Approved = "APPROVED", Rejected = "REJECTED"}This compiles to:
var Status;(function (Status) { Status["Pending"] = "PENDING"; Status["Approved"] = "APPROVED"; Status["Rejected"] = "REJECTED";})(Status || (Status = {}));Better — no reverse mapping. But you're still shipping a runtime object for something that could be purely a compile-time construct.
---
Problem #3: Enums Are Not Tree-Shakeable
Modern bundlers like Webpack, Rollup, and esbuild can "tree-shake" — removing unused code from your final bundle. Enums break this.
// config.tsexport enum Colors { Red = "RED", Green = "GREEN", Blue = "BLUE", Yellow = "YELLOW", Orange = "ORANGE", Purple = "PURPLE", // ... 50 more colors}
// app.tsimport { Colors } from "./config";console.log(Colors.Red);Even though you only use Colors.Red, the entire enum object ships to production. Bundlers can't safely remove individual enum members because they're all part of one runtime object.
---
Problem #4: Numeric Enums Are Dangerous
The default numeric enum behavior is a footgun:
enum Status { Pending, // 0 Approved, // 1 Rejected // 2}
function processStatus(status: Status) { console.log(status);}
// This should be an error, but TypeScript allows it!processStatus(42); // ✅ No error!processStatus(999); // ✅ No error!Wait, what? TypeScript lets you pass ANY number where an enum is expected. This defeats the entire purpose of type safety.
This happens because numeric enums are implemented as numbers at runtime, and TypeScript's type system doesn't prevent arbitrary number assignment.
---
Problem #5: Const Enums Have Their Own Issues
TypeScript offers const enum as a "fix":
const enum Status { Pending, Approved, Rejected}
const status = Status.Approved;Compiles to:
const status = 1; // Inlined!No runtime object — the values are inlined. Sounds great, but:
Problem 5a: Const Enums Break When Published
If you publish a library with const enum, consumers using --isolatedModules (required for Babel, esbuild, swc) will get errors. The enum values can't be inlined across package boundaries.
Cannot access ambient const enums when the '--isolatedModules' flag is provided.This is why the TypeScript team recommends avoiding const enum in libraries.
Problem 5b: Const Enums Break Source Maps
Since values are inlined, debugging becomes harder. The debugger shows 1 instead of Status.Approved.
Problem 5c: Const Enums Require preserveConstEnums in Monorepos
In monorepos, you often need preserveConstEnums: true, which... generates the full enum anyway, defeating the purpose.
---
Problem #6: Enums Create a Type AND a Value
This is subtle but causes confusion:
enum Status { Pending, Approved, Rejected}
// Status is both a TYPE and a VALUEtype StatusType = Status; // Using as typeconst obj = Status; // Using as valueconst pending = Status.Pending; // Accessing value
// This duality causes confusion in type-level codetype Keys = keyof typeof Status; // Need "typeof" - confusing!The fact that enums exist in both the type space and value space creates cognitive overhead and edge cases that bite you in complex type manipulations.
---
Problem #7: Enums Don't Play Well With Object Types
You can't easily derive types from enums or use them in mapped types without awkward workarounds:
enum Status { Pending = "PENDING", Approved = "APPROVED", Rejected = "REJECTED"}
// Want a type with Status values as keys? It's ugly:type StatusMap = { [K in Status]: boolean;};
// Type error! K is the VALUE ("PENDING"), not the key name// This creates: { "PENDING": boolean, "APPROVED": boolean, ... }// When you might want: { Pending: boolean, Approved: boolean, ... }---
The Solution: Union Types + as const
Here's the modern approach that solves all these problems:
const Status = { Pending: "PENDING", Approved: "APPROVED", Rejected: "REJECTED",} as const;
type Status = (typeof Status)[keyof typeof Status];// type Status = "PENDING" | "APPROVED" | "REJECTED"Why This Is Better
1. Zero runtime overhead
The as const assertion is compile-time only. The object compiles to exactly what you wrote:
const Status = { Pending: "PENDING", Approved: "APPROVED", Rejected: "REJECTED",};2. Fully tree-shakeable
Bundlers can analyze object property access and remove unused values.
3. True type safety
function processStatus(status: Status) { console.log(status);}
processStatus("PENDING"); // ✅processStatus("INVALID"); // ❌ Error: Argument of type '"INVALID"' is not assignableprocessStatus(42); // ❌ Error: Argument of type '42' is not assignable4. Works everywhere
No isolatedModules issues, no build configuration problems. It's just JavaScript objects and TypeScript types.
---
The Pattern: Making It Reusable
Create a helper type to reduce boilerplate:
// Helper typetype ValueOf<T> = T[keyof T];
// Define your constantsconst Status = { Pending: "PENDING", Approved: "APPROVED", Rejected: "REJECTED",} as const;
// Derive the union typetype Status = ValueOf<typeof Status>;Or use the built-in approach:
const Status = { Pending: "PENDING", Approved: "APPROVED", Rejected: "REJECTED",} as const;
type Status = (typeof Status)[keyof typeof Status];---
Handling Numeric Constants
If you need numeric values (like HTTP status codes):
const HttpStatus = { OK: 200, Created: 201, BadRequest: 400, Unauthorized: 401, NotFound: 404, InternalServerError: 500,} as const;
type HttpStatus = (typeof HttpStatus)[keyof typeof HttpStatus];// type HttpStatus = 200 | 201 | 400 | 401 | 404 | 500
function handleResponse(status: HttpStatus) { if (status === HttpStatus.OK) { // TypeScript knows status is exactly 200 here }}
handleResponse(200); // ✅handleResponse(999); // ❌ Error!Unlike numeric enums, arbitrary numbers are rejected!
---
Getting Object Keys As a Type
Need the key names as a type? Easy:
const Status = { Pending: "PENDING", Approved: "APPROVED", Rejected: "REJECTED",} as const;
type StatusKey = keyof typeof Status;// type StatusKey = "Pending" | "Approved" | "Rejected"
type StatusValue = (typeof Status)[keyof typeof Status];// type StatusValue = "PENDING" | "APPROVED" | "REJECTED"---
Real-World Example: API Response Types
const ApiErrorCode = { InvalidInput: "INVALID_INPUT", Unauthorized: "UNAUTHORIZED", NotFound: "NOT_FOUND", RateLimited: "RATE_LIMITED", ServerError: "SERVER_ERROR",} as const;
type ApiErrorCode = (typeof ApiErrorCode)[keyof typeof ApiErrorCode];
interface ApiError { code: ApiErrorCode; message: string; timestamp: number;}
// Usageconst error: ApiError = { code: ApiErrorCode.NotFound, message: "User not found", timestamp: Date.now(),};
// Or use string directly (both work!)const error2: ApiError = { code: "NOT_FOUND", message: "User not found", timestamp: Date.now(),};---
What About Iteration?
Enums let you iterate over values. So do objects:
const Status = { Pending: "PENDING", Approved: "APPROVED", Rejected: "REJECTED",} as const;
// Get all valuesconst allStatuses = Object.values(Status);// ["PENDING", "APPROVED", "REJECTED"]
// Get all keysconst allKeys = Object.keys(Status) as (keyof typeof Status)[];// ["Pending", "Approved", "Rejected"]
// Iteratefor (const status of Object.values(Status)) { console.log(status);}---
Migration Guide: Enum to Const Object
If you have existing enums, here's how to migrate:
Before (String Enum)
enum UserRole { Admin = "ADMIN", Editor = "EDITOR", Viewer = "VIEWER",}
function hasPermission(role: UserRole): boolean { return role === UserRole.Admin;}After (Const Object)
const UserRole = { Admin: "ADMIN", Editor: "EDITOR", Viewer: "VIEWER",} as const;
type UserRole = (typeof UserRole)[keyof typeof UserRole];
function hasPermission(role: UserRole): boolean { return role === UserRole.Admin;}The calling code doesn't need to change! Both UserRole.Admin and "ADMIN" work as values.
---
When ARE Enums Acceptable?
Honestly? Almost never. But if you insist:
- String enums only — never numeric
- Internal code only — never in published libraries
- Small, stable sets — not 50+ values that might tree-shake
- When reverse mapping is needed — rare but exists
Even then, I'd still recommend as const objects.
---
Summary
| Feature | Enum | `as const` Object |
|---|---|---|
| Runtime code | Yes (IIFE) | Minimal (plain object) |
| Tree-shakeable | ❌ No | ✅ Yes |
| Type safety (numbers) | ⚠️ Weak | ✅ Strong |
| Works with isolatedModules | ⚠️ Issues | ✅ Yes |
| Library-safe | ⚠️ Problematic | ✅ Yes |
| Debugging | ⚠️ Const enum issues | ✅ Clear |
TL;DR
Stop using enums. Use as const objects instead.
// ❌ Don'tenum Status { Pending = "PENDING", Approved = "APPROVED",}
// ✅ Doconst Status = { Pending: "PENDING", Approved: "APPROVED",} as const;
type Status = (typeof Status)[keyof typeof Status];Your bundles will be smaller, your types will be safer, and your code will be more predictable.
---
Follow @codingwithshahar for more TypeScript tips!
Stay Updated 📬
Get the latest tips and tutorials delivered to your inbox. No spam, unsubscribe anytime.