Enums and const objects

TypeScript · Practical · 12 min

You will meet enums in existing code, and choosing the right replacement affects bundles, tooling and interop.

An enum is one of the few TypeScript features that emits runtime code. It declares a set of named constants:

enum Direction { Up, Down }            // numeric: Up = 0, Down = 1
enum Level { Info = "info", Warn = "warn" } // string enum

Numeric enums create a two-way object (Direction[0] === "Up") and are loosely typed — older TypeScript versions even let any number be assigned to a numeric enum. String enums are stricter but nominal: a function taking Level refuses the plain string "info", which is awkward when values come from JSON. const enum inlines values at compile time but breaks under tools that compile one file at a time (isolatedModules).

Because enums aren't plain JavaScript, they also don't work with type-stripping runtimes (Node's built-in TypeScript support) and are disallowed by the erasableSyntaxOnly option. Many teams therefore prefer two alternatives:

  • A union of string literals: type Level = "info" | "warn". No runtime footprint, plain strings everywhere, great autocomplete. Ideal when you don't need to iterate the values.
  • An `as const` object plus a derived type, when you need the values at runtime too (for a dropdown, validation, iteration):
const Level = { Info: "info", Warn: "warn" } as const;
type Level = (typeof Level)[keyof typeof Level]; // "info" | "warn"

as const makes the properties readonly literal types, typeof gets the object's type, and the indexed access collects the values into a union. Using the same name for the value and the type is legal and reads naturally: Level.Info at runtime, Level as a type.

Enums are still fine in codebases that use them consistently — just know their quirks.

Example

// Classic string enum (emits a runtime object)
enum LogLevel { Info = "info", Warn = "warn", Error = "error" }
function logEnum(level: LogLevel, msg: string) { console.log(`[${level}] ${msg}`); }
logEnum(LogLevel.Warn, "disk low");
// @ts-expect-error -- plain strings aren't accepted for a string enum
logEnum("warn", "disk low");

// Alternative: const object + derived union type
const Level = { Info: "info", Warn: "warn", Error: "error" } as const;
type Level = (typeof Level)[keyof typeof Level];   // "info" | "warn" | "error"

function log(level: Level, msg: string) { console.log(`[${level}] ${msg}`); }
log(Level.Error, "boom");
log("info", "plain strings work too");

const allLevels = Object.values(Level);              // iterate at runtime
console.log(allLevels);                              // ["info", "warn", "error"]

Remember

  • Enums emit runtime code; string enums are nominal
  • Prefer literal unions when you only need types
  • as const object + (typeof X)[keyof typeof X] when you need runtime values too

Watch out for

  • Using const enum with isolatedModules-based tools (esbuild, Babel, swc), where it can't be inlined safely.

Check yourself

  1. Given const Size = { S: "s", L: "l" } as const, what is (typeof Size)[keyof typeof Size]?
  2. Why can't Node's built-in type stripping run a file containing an enum?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.typescriptlang.org/docs/handbook/enums.html