Skip to main content

Why is enum often considered an antipattern in TS?

In short: why enum is often called an antipattern

Because enum is "runtime magic" that hurts type safety, adds bulk to the code, and integrates worse with the JS ecosystem than alternatives like const enum or as const objects.


1. enum exists both at runtime and at compile time

The main feature (and problem) of enum is that it generates JavaScript code at compile time.

Example:

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

Compiles to JS as:

javascript
"use strict"; 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 = {}));

That is, enum is not a purely type-level construct, it generates a real object, which:

  • adds extra code to the bundle,
  • can behave differently than you'd expect.

2. A two-way mapping (reverse mapping) = a potential bug

TypeScript creates a two-way table:

javascript
Direction.Up === 0 Direction[0] === "Up"

This is convenient, but dangerous:

javascript
enum Role { Admin, User } const role = Role[0]; // "Admin"

Values can be reversed, which creates hidden vulnerabilities and confusion.


3. Enum values are not type-safe

An enum behaves almost like an object with number | string values, and TypeScript often does not protect against assignment errors.

Example:

javascript
enum Role { Admin, User } const role: Role = 42; // allowed (!)

This is not obvious and breaks the point of an enum, because 42 is not an existing value, but TS does not filter it out.


4. Mixing types (numeric + string enums)

You can write this:

javascript
enum Mixed { Yes = "YES", No = 0, }

Combining numbers and strings like this leads to unpredictable behavior and makes type checking harder.


5. enum does not work well with tree-shaking

Since enum compiles to a real JS object, it cannot be removed by the bundler's optimizer (for example, Terser or esbuild).

Example:

javascript
enum Colors { Red, Blue, Green }

Even if Colors is not used, it still ends up in the bundle.

But an object with as const is removed if it is not used:

javascript
const Colors = { Red: 0, Blue: 1, Green: 2 } as const;

6. Poor compatibility with plain JS / JSON / APIs

An enum is a runtime object, and its values do not match plain strings.

Example:

javascript
enum Status { Active = "active", Inactive = "inactive" } function setStatus(status: Status) { ... } setStatus("active"); // Error - TS expects Status.Active

Although the string "active" matches in meaning, TypeScript requires exactly Status.Active. This gets in the way of using an enum with data from an API.

It is better to use a string literal:

javascript
type Status = "active" | "inactive";

7. Difficulties with serialization and debugging

javascript
enum Status { Active, Inactive } JSON.stringify(Status); // {"0":"Active","1":"Inactive","Active":0,"Inactive":1}

Instead of a simple enumeration, you get a useless jumble that's hard to pull the needed values out of.


8. Enum behavior breaks the "value-level vs type-level" concept

TypeScript normally separates:

  • type level (only at compile time)
  • value level (at runtime)

enum mixes these two levels.

Example:

javascript
enum Fruit { Apple, Orange } function eat(fruit: Fruit) { ... } const a = Fruit.Apple; // a variable and a type in one

This is "magic" that breaks TS's predictability principles.


9. An alternative: as const + typeof

A more modern and safer approach is literal objects with as const.

Example:

javascript
const Direction = { Up: "up", Down: "down", Left: "left", Right: "right", } as const; type Direction = typeof Direction[keyof typeof Direction];

Advantages:

  • Generates no JS code (only types);
  • Works with APIs and JSON;
  • Tree-shaking friendly;
  • Does not break typing.

10. const enum - a compromise (but with its own nuances too)

If you still need an enum, it's better to use const enum:

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

TypeScript inlines the values directly:

javascript
const move = Direction.Up; // compiles to: const move = 0;

But:

  • it does not work with isolatedModules: true (for example, with Babel);
  • it breaks debugging (values turn into numbers);
  • it can be incompatible with other tools (for example, SWC, ts-node).

11. enum breaks the idea of "a type as a contract document"

For example, when you use enum in a public API, consumers see numeric codes rather than literal values. This makes the type less self-documenting.

Example:

javascript
enum Status { Success, Fail } function getStatus(): Status { ... }

Returns 0 | 1, but the IDE will not hint "Success" | "Fail" - understanding the code becomes harder.


12. Summary: when enum is actually needed

enum is justified only in rare cases:

  • for compatibility with existing C#/Java code;
  • when working with external libraries where an enum is part of the API;
  • in specific compile-time scenarios (const enum).

For everything else: use a string literal union or an as const object.


Final comparison table

ApproachGenerates code?Safe?Convenient with APIs?Tree-shakingRecommended
enumYesPartiallyNoNoNo
const enumNo (inline)YesPartiallyYesWith caution
as const objectNoYesYesYesRecommended
A union of types (`'a''b'`)NoYesYesYes

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.