Enums define a closed set of named constants — think HTTP status codes, user roles, or order states. TypeScript gives you three ways to model them: traditional enums, const enum, and the increasingly popular as const object pattern. Each has real trade-offs around bundle size, type safety, and developer ergonomics, and picking the wrong one can bite you in production.
Master typescript enums
Take the TypeScript Essentials course with hands-on lessons and challenges.
Numeric enums make sense for HTTP status codes because the numeric values carry real meaning — you might compare against ranges (>= 500) or send them over the wire. This is one of the few cases where numeric enums are the right call.
String enums shine for domain values that cross API boundaries. The SCREAMING_CASE values match what your backend sends, and the enum type prevents you from passing arbitrary strings into business logic functions.
The as const pattern produces a plain object with literal types. Unlike enums, you can pass either the constant reference (LogLevel.Warn) or the raw string ("error") — both are valid. This flexibility is why many teams prefer it.
const enum inlines every reference at compile time — Direction.Left becomes the string "LEFT" directly. No runtime object is created. The catch: this only works with TypeScript's own compiler. Babel, SWC, and esbuild either ignore const enum or need special configuration.
For small, stable sets of values, a plain union type is the simplest solution. You get autocomplete, exhaustiveness checking in switch statements, and zero runtime overhead. No import needed — just use the string directly.
Enums pair well with discriminated unions. The enum value acts as the discriminant, and TypeScript narrows the type in each switch branch. This pattern is common in error handling, event systems, and state machines.
Using numeric enums for values that cross API boundaries — your backend sends `"PENDING"` but your enum member has value `0`
Use string enums or `as const` when values are serialized to JSON. Numeric enums only make sense when the number itself has meaning (HTTP status codes, priority levels with numeric comparisons).
Passing a raw string where an enum type is expected — `updateStatus("SHIPPED")` fails even though `OrderStatus.Shipped === "SHIPPED"`
Enums are nominally typed: you must use the enum member (`OrderStatus.Shipped`), not the raw string. If you want to accept both, use `as const` objects instead, which produce a union of string literals.
Using `const enum` in a library published to npm — consumers using Babel or `--isolatedModules` will get errors because the enum doesn't exist at runtime
Avoid `const enum` in any code that will be consumed by other packages. Use regular string enums or `as const` objects instead. The TypeScript team themselves recommend against `const enum` in library code.
Relying on numeric enum reverse mapping as a validation mechanism — `if (MyEnum[value] !== undefined)` accepts any number, not just valid members
Numeric enums map every number to `undefined` for non-members but also create reverse mappings for valid ones. For validation, use `Object.values(MyEnum).includes(value)` or switch to string enums where the value space is constrained.
TypeScript gives you four ways to model a fixed set of values: numeric enums, string enums, `const enum`, and `as const` objects. String enums are the safe default for most domain modeling. `as const` objects are the modern choice when you want flexibility and clean JavaScript output. Numeric enums are reserved for cases where the number matters (like HTTP status codes). And plain union types are the simplest option when you just need a type constraint without a runtime object. Pick based on whether you need runtime iteration, cross-boundary serialization, or just compile-time checking.
Interactive lessons and challenges on Stanza, practice in VS Code, Cursor, or the web.
Interactive lessons and challenges, right in your code editor.
Check the free courses. No credit card.