DEV Community

Cover image for The Code Review That Changed Everything
Elvis Sautet
Elvis Sautet

Posted on

The Code Review That Changed Everything

Three months ago, I submitted what I thought was a perfectly reasonable pull request. I had created a new UserRole enum to handle our permission system. Clean, type-safe, idiomatic TypeScript.

The senior engineer's review came back with one comment: "Please don't use enums."

I was confused. Enums are in the TypeScript handbook. They're taught in every course. Major codebases use them. What was wrong with enums?

Then he showed me the compiled JavaScript output.

I deleted every enum from our codebase that afternoon.

This article explains why TypeScript enums are one of the language's most misunderstood features—and why you should probably stop using them.


Part 1: The Enum Illusion

TypeScript sells itself as "JavaScript with syntax for types." The promise is simple: write TypeScript, get type safety, compile to clean JavaScript.

For most TypeScript features, this is true. Interfaces? Erased. Type annotations? Erased. Generics? Erased.

Enums? They become real runtime code.

This fundamental difference makes enums an anomaly in TypeScript—and a trap for developers who don't understand the compilation model.

The Simple Example

Let's start with something innocent:

enum Status {
  Active = "ACTIVE",
  Inactive = "INACTIVE",
  Pending = "PENDING"
}

function getUserStatus(): Status {
  return Status.Active
}
Enter fullscreen mode Exit fullscreen mode

Looks clean, right? Here's what actually ships to your users:

var Status;
(function (Status) {
  Status["Active"] = "ACTIVE";
  Status["Inactive"] = "INACTIVE";
  Status["Pending"] = "PENDING";
})(Status || (Status = {}));

function getUserStatus() {
  return Status.Active;
}
Enter fullscreen mode Exit fullscreen mode

That's 9 lines of JavaScript for 5 lines of TypeScript.

But wait—it gets worse.


Part 2: The Numeric Enum Nightmare

String enums are bad. Numeric enums are a disaster.

enum Role {
  Admin,
  User,
  Guest
}
Enter fullscreen mode Exit fullscreen mode

You might expect this to compile to something simple. Maybe const Role = { Admin: 0, User: 1, Guest: 2 }.

Here's what you actually get:

var Role;
(function (Role) {
  Role[Role["Admin"] = 0] = "Admin";
  Role[Role["User"] = 1] = "User";
  Role[Role["Guest"] = 2] = "Guest";
})(Role || (Role = {}));
Enter fullscreen mode Exit fullscreen mode

What's happening here?

TypeScript is creating reverse mappings. The compiled object looks like this:

{
  Admin: 0,
  User: 1,
  Guest: 2,
  0: "Admin",
  1: "User",
  2: "Guest"
}
Enter fullscreen mode Exit fullscreen mode

This allows you to do: Role[0] // "Admin"

Question: Did you ever need this feature?

In five years of professional TypeScript development, I have never once needed to look up an enum name from its numeric value. Not once.

Yet I've shipped this extra code to production hundreds of times.


Part 3: The Tree-Shaking Problem

Modern bundlers like Webpack, Rollup, and Vite have sophisticated tree-shaking capabilities. They can eliminate unused code with surgical precision.

Unless you're using enums.

The Problem

// types.ts
export enum Status {
  Active = "ACTIVE",
  Inactive = "INACTIVE",
  Pending = "PENDING",
  Archived = "ARCHIVED",
  Deleted = "DELETED"
}

// app.ts
import { Status } from './types'

const currentStatus = Status.Active
Enter fullscreen mode Exit fullscreen mode

What you want: Just the string "ACTIVE" in your bundle.

What you get: The entire Status enum object plus the IIFE wrapper.

Enums cannot be tree-shaken because they're runtime constructs. Even if you only use one value, you get all of them.

Multiply this across dozens of enums in a real application, and you're shipping kilobytes of unnecessary code.


Part 4: The Better Alternative

So if enums are problematic, what should we use instead?

Solution 1: Const Objects with 'as const'

const Status = {
  Active: "ACTIVE",
  Inactive: "INACTIVE",
  Pending: "PENDING"
} as const
Enter fullscreen mode Exit fullscreen mode

Compiled JavaScript:

const Status = {
  Active: "ACTIVE",
  Inactive: "INACTIVE",
  Pending: "PENDING"
}
Enter fullscreen mode Exit fullscreen mode

That's it. No IIFE. No runtime overhead. Just a simple object.

Creating the Type

type Status = typeof Status[keyof typeof Status]
// Expands to: type Status = "ACTIVE" | "INACTIVE" | "PENDING"
Enter fullscreen mode Exit fullscreen mode

Now you have:

  • ✅ A runtime object for values
  • ✅ A compile-time type for type checking
  • ✅ Zero compilation overhead
  • ✅ Tree-shakeable (if your bundler supports it)

Usage

// Works exactly like enums:
function setStatus(status: Status) {
  console.log(status)
}

setStatus(Status.Active) // ✅ Valid
setStatus("ACTIVE")      // ✅ Valid (it's just a string)
setStatus("INVALID")     // ❌ Type error
Enter fullscreen mode Exit fullscreen mode

Part 5: The Type Safety Advantage

Here's where it gets interesting: const objects provide BETTER type safety than enums.

The Enum Problem

enum Color {
  Red = 0,
  Blue = 1
}

enum Status {
  Inactive = 0,
  Active = 1
}

function setColor(color: Color) {
  console.log(`Color: ${color}`)
}

// This compiles successfully:
setColor(Status.Active) // No error!
Enter fullscreen mode Exit fullscreen mode

Why? Because TypeScript enums use structural typing. Both Color and Status are numbers, so TypeScript considers them compatible.

This compiled and shipped to production. It caused a bug that took hours to debug.

The Object Solution

const Color = {
  Red: "RED",
  Blue: "BLUE"
} as const

const Status = {
  Inactive: "INACTIVE",
  Active: "ACTIVE"
} as const

type Color = typeof Color[keyof typeof Color]

function setColor(color: Color) {
  console.log(`Color: ${color}`)
}

// Type error:
setColor(Status.Active) // ❌ Type '"ACTIVE"' is not assignable to type '"RED" | "BLUE"'
Enter fullscreen mode Exit fullscreen mode

The const object approach uses literal types, which are exact string values. TypeScript catches the error at compile time.

Const objects provide stricter type checking than enums.


Part 6: The Migration Path

Convinced? Here's how to migrate existing enums.

Step 1: Identify String Enums

These are the easiest to migrate:

// Before
enum Status {
  Active = "ACTIVE",
  Inactive = "INACTIVE"
}

// After
const Status = {
  Active: "ACTIVE",
  Inactive: "INACTIVE"
} as const

type Status = typeof Status[keyof typeof Status]
Enter fullscreen mode Exit fullscreen mode

Step 2: Convert Numeric Enums

For numeric enums, you need to preserve the numbers:

// Before
enum HttpStatus {
  OK = 200,
  NotFound = 404,
  ServerError = 500
}

// After
const HttpStatus = {
  OK: 200,
  NotFound: 404,
  ServerError: 500
} as const

type HttpStatus = typeof HttpStatus[keyof typeof HttpStatus]
Enter fullscreen mode Exit fullscreen mode

Step 3: Update Usage

The good news? Usage stays mostly the same:

// Both work identically:
const status1: Status = Status.Active
const status2: HttpStatus = HttpStatus.OK

// Pattern matching still works:
switch (status) {
  case Status.Active:
    // ...
  case Status.Inactive:
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Handle Edge Cases

If you're using reverse lookups (rare), you'll need to create an explicit reverse map:

const HttpStatus = {
  OK: 200,
  NotFound: 404
} as const

// Create reverse mapping only if needed:
const HttpStatusNames = {
  200: "OK",
  404: "NotFound"
} as const

HttpStatusNames[200] // "OK"
Enter fullscreen mode Exit fullscreen mode

Part 7: The One Exception

Is there ever a valid reason to use enums?

Maybe: const enums

const enum Direction {
  Up,
  Down,
  Left,
  Right
}

const move = Direction.Up
Enter fullscreen mode Exit fullscreen mode

Compiles to:

const move = 0 /* Direction.Up */
Enter fullscreen mode Exit fullscreen mode

Const enums are inlined at compile time. They don't create runtime objects.

However:

  1. They don't work with isolatedModules (required for Babel, esbuild, SWC)
  2. They're being deprecated in favor of preserveConstEnums
  3. They're more complex than just using objects

My recommendation: Even for const enums, just use objects. Simpler is better.


Part 8: Real-World Impact

When we migrated our codebase from enums to const objects, here's what happened:

Before Migration

  • Enums in codebase: 47
  • Bundle size: 2.4 MB (minified)
  • Enum-related code in bundle: ~14 KB

After Migration

  • Enums in codebase: 0
  • Bundle size: 2.388 MB (minified)
  • Savings: 12 KB

"Only 12KB?"

Yes, but:

  1. It's 12KB we don't need to ship, parse, or execute
  2. Type safety improved (we caught 3 bugs during migration)
  3. Code became more readable (it's just JavaScript)
  4. New developers onboard faster (fewer TypeScript quirks)

Developer Experience Improvements

  1. Faster compilation: TypeScript doesn't need to generate enum code
  2. Better IDE performance: Fewer runtime constructs to track
  3. Easier debugging: Console logs show actual values, not enum references
  4. Simpler mental model: One less TypeScript-specific feature to remember

Part 9: Common Objections

"But enums are in the TypeScript docs!"

So are namespaces, and those are also considered legacy. The TypeScript team has acknowledged that enums were a mistake, but they can't remove them without breaking changes.

"My entire codebase uses enums!"

Migration is straightforward and can be done incrementally. Start with new code, migrate old code during refactors.

"Enums are more explicit!"

// Enum
enum Status { Active = "ACTIVE" }

// Object
const Status = { Active: "ACTIVE" } as const
Enter fullscreen mode Exit fullscreen mode

The difference is minimal. The object version is actually more JavaScript-idiomatic.

"I need the type and the value!"

You get both with the const object pattern:

const Status = { Active: "ACTIVE" } as const  // Runtime value
type Status = typeof Status[keyof typeof Status]  // Compile-time type
Enter fullscreen mode Exit fullscreen mode

"What about JSON serialization?"

Enums serialize to their underlying values anyway:

enum Status { Active = "ACTIVE" }
JSON.stringify({ status: Status.Active }) // {"status":"ACTIVE"}
Enter fullscreen mode Exit fullscreen mode

Same as:

const Status = { Active: "ACTIVE" } as const
JSON.stringify({ status: Status.Active }) // {"status":"ACTIVE"}
Enter fullscreen mode Exit fullscreen mode

No difference.


Part 10: The Philosophical Point

TypeScript's motto is "JavaScript that scales." The best TypeScript code is code that looks like JavaScript but with type annotations.

Enums violate this principle. They're a TypeScript-only construct that generates runtime code and behaves differently from anything in JavaScript.

When in doubt, prefer JavaScript idioms with TypeScript types over TypeScript-specific features.

Good TypeScript:

const Status = { Active: "ACTIVE" } as const
type Status = typeof Status[keyof typeof Status]
Enter fullscreen mode Exit fullscreen mode

This is JavaScript (an object) with TypeScript types. It scales. It's familiar. It works everywhere.

Questionable TypeScript:

enum Status { Active = "ACTIVE" }
Enter fullscreen mode Exit fullscreen mode

This is TypeScript-specific syntax that generates unexpected runtime code.


Conclusion: Make the Switch

TypeScript enums seemed like a good idea in 2012. In 2025, we have better options.

The case against enums:

  • ❌ Generate unexpected runtime code
  • ❌ Don't tree-shake
  • ❌ Create reverse mappings nobody uses
  • ❌ Weaker type safety than literal types
  • ❌ TypeScript-specific syntax

The case for const objects:

  • ✅ Zero runtime overhead
  • ✅ Tree-shakeable
  • ✅ Just JavaScript
  • ✅ Stronger type safety
  • ✅ Works everywhere

Next time you reach for an enum, reach for a const object instead.

Your bundle will be smaller. Your types will be stricter. Your code will be clearer.

Stop using enums. Start using objects.


Quick Reference Guide

String Enum Migration

// ❌ Old way
enum Status {
  Active = "ACTIVE",
  Inactive = "INACTIVE"
}

// ✅ New way
const Status = {
  Active: "ACTIVE",
  Inactive: "INACTIVE"
} as const

type Status = typeof Status[keyof typeof Status]
Enter fullscreen mode Exit fullscreen mode

Numeric Enum Migration

// ❌ Old way
enum Priority {
  Low = 1,
  Medium = 2,
  High = 3
}

// ✅ New way
const Priority = {
  Low: 1,
  Medium: 2,
  High: 3
} as const

type Priority = typeof Priority[keyof typeof Priority]
Enter fullscreen mode Exit fullscreen mode

Helper Type for Reusability

// Create a reusable type helper
type ValueOf<T> = T[keyof T]

const Status = {
  Active: "ACTIVE",
  Inactive: "INACTIVE"
} as const

type Status = ValueOf<typeof Status>
Enter fullscreen mode Exit fullscreen mode

Further Reading


About Me

I'm a senior TypeScript developer with 5+ years of experience building production applications. I learned this lesson the hard way—by shipping unnecessary enum code to millions of users. Now I share what I've learned so you don't have to make the same mistakes.

If you found this helpful, consider sharing it with your team. The more developers who understand this, the better code we'll all ship.

Top comments (1)

Collapse
 
aoda-zhang profile image
Aoda Zhang

Nice post,actually i have used enum many times,never considered what it will translated to js,great,i will change it to cost,thx