TypeScript Enums: When to Use Them and When to Avoid Them
An enum gives names to a set of constants. Instead of scattering magic strings or numbers across your codebase, you define them once and reference them ev...
23 Apr 2024

Enums are one of the first features people reach for in TypeScript, and one of the first that experienced teams start banning. Both reactions are right, for different reasons. Here is the staff-level view: what enums actually compile to, where they bite, and what to use instead.
What a normal enum compiles to
Unlike most of TypeScript, a plain enum is not erased at compile time. It emits real JavaScript:
enum Direction {
Up,
Down,
}
becomes an IIFE that builds a two-way lookup object:
var Direction;
(function (Direction) {
Direction[(Direction["Up"] = 0)] = "Up";
Direction[(Direction["Down"] = 1)] = "Down";
})(Direction || (Direction = {}));
That reverse mapping (Direction[0] === "Up") is occasionally useful and usually just weight your bundle carries for no reason. It is also why enums cannot be tree-shaken away when unused.
The runtime-erasure problem
This is the big one in 2026. Node.js can now run TypeScript directly by stripping types, and the same "type-stripping" approach powers fast tools across the ecosystem. Type stripping only works if every TypeScript construct can be erased without changing runtime behavior. A normal enum emits code, so it cannot be stripped. TypeScript added the erasableSyntaxOnly flag precisely to forbid enums (and other code-emitting constructs) so your project stays compatible with type-stripping runtimes.
If you want your code to run under node --experimental-strip-types or its stable successor, plain enums are a liability.
const enums do not fix it
const enum inlines its values and emits nothing:
const enum Size { Small, Large }
const s = Size.Large; // compiles to: const s = 1;
Smaller output, but it breaks in other ways: it requires whole-program knowledge, so it fails under isolated-module compilation (Babel, esbuild, swc, and again type stripping) unless you enable extra flags, and it cannot cross a package boundary safely. Most style guides ban const enum for exactly this reason.
What to use instead
A union of string literals covers the majority of cases with zero runtime footprint:
type Direction = "up" | "down";
function move(d: Direction) { /* ... */ }
move("up"); // autocompletes, type-checks, emits nothing
A const object with a derived type when you need a named, iterable set of values:
const Direction = {
Up: "up",
Down: "down",
} as const;
type Direction = (typeof Direction)[keyof typeof Direction]; // "up" | "down"
You get the ergonomics people actually wanted from enums: a Direction.Up reference, iterable keys via Object.values(Direction), and a precise union type, but it is a plain object that tree-shakes and erases cleanly.
When an enum is still fine
This is not an absolute ban. If you are shipping an application (not a library), compiling with tsc, and not targeting a type-stripping runtime, enums are convenient and the downsides are theoretical. The rules that matter:
- Libraries: avoid enums in your public API. A consumer using esbuild or type stripping should not inherit your compilation constraints.
- Apps targeting type stripping: turn on
erasableSyntaxOnlyand use unions orconstobjects. - Everywhere: prefer string enums over numeric ones if you do use them, so values are stable and debuggable, and never rely on the numeric reverse mapping.
Wrapping up
The reason enums feel wrong to experienced TypeScript engineers is that they violate the language's core promise: types disappear at runtime, enums do not. In 2026, with type-stripping runtimes becoming the norm, that gap turned from a style opinion into a compatibility issue. Reach for a string-literal union first, a const object when you need a value bag, and keep enums out of anything you publish.
From Senior to Staff: Master the Architecture Skills That Get You Promoted
Go from shaky in design reviews to the engineer everyone trusts to architect the hard stuff.
View the live cohortKeep reading
- The Frontend Toolchain Is Now Written in Rust and Go. What That Means for You
- oxlint vs ESLint: Why I Replaced ESLint with oxlint
- SystemJS Is Dead. Native ESM Finally Won.
- Replace Axios with a Simple Custom Fetch Wrapper (Production-Ready Guide)
- Stop Shipping ChatGPT Wrappers. Ship an Agent in TypeScript, or Don't Bother.
- What Developers Are Saying About React 19: Pros and Cons