Enums in TypeScript Explained

An enum is a way to define a set of named constants in TypeScript. Learn numeric enums, string enums, const enums, and when to use enums versus union types.

7 min read

Enums in TypeScript let you define a group of named constants under one name. Instead of passing around raw strings or numbers like "admin" or 1, you give each value a meaningful name and use that name throughout your code.

Here is a basic enum for user roles:

typescripttypescript
enum Role {
  Admin,
  Editor,
  Viewer,
}

This declaration creates three named constants grouped under Role, each automatically assigned a number behind the scenes. You can now use Role.Admin, Role.Editor, and Role.Viewer anywhere a value is needed:

typescripttypescript
function getPermissions(role: Role): string[] {
  if (role === Role.Admin) {
    return ["read", "write", "delete"];
  }
  return ["read"];
}
 
console.log(getPermissions(Role.Admin));  // ["read", "write", "delete"]
console.log(getPermissions(Role.Viewer)); // ["read"]

The enum replaces magic values with named constants. The code is easier to read, and annotating the role parameter with Role means TypeScript only accepts a valid member, the same way it checks any other type annotation.

Enums are one of the few TypeScript features that produce runtime code. Unlike type annotations and interfaces, which disappear during compilation, a regular enum becomes a JavaScript object.

Numeric Enums

Numeric enums are the default form, and the Role enum above is already one. When you list members without assigning any values, TypeScript assigns auto-incrementing numbers starting from 0 in the order the members appear:

typescripttypescript
enum Direction {
  Up,    // 0
  Down,  // 1
  Left,  // 2
  Right, // 3
}
 
console.log(Direction.Up);    // 0
console.log(Direction.Right); // 3

Assigning a starting value changes the whole sequence: TypeScript still auto-increments each following member, but it counts up from whatever number you gave the first one instead of zero:

typescripttypescript
enum Status {
  Pending = 1,
  Active,   // 2
  Closed,   // 3
}
 
console.log(Status.Pending); // 1
console.log(Status.Active);  // 2

Every member can also take its own specific value instead of relying on auto-increment at all, which is common when the numbers need to match an external system:

typescripttypescript
enum HttpStatus {
  OK = 200,
  Created = 201,
  NotFound = 404,
  ServerError = 500,
}

Reverse Mapping

Numeric enums have a unique feature called reverse mapping. You can look up the name of an enum member from its numeric value:

typescripttypescript
enum Color {
  Red,
  Green,
  Blue,
}
 
console.log(Color.Red);   // 0
console.log(Color[0]);    // "Red"
console.log(Color[1]);    // "Green"

This works because TypeScript generates a JavaScript object that maps both ways: name to value and value to name. The compiled JavaScript looks like this:

javascriptjavascript
var Color;
(function (Color) {
  Color[Color["Red"] = 0] = "Red";
  Color[Color["Green"] = 1] = "Green";
  Color[Color["Blue"] = 2] = "Blue";
})(Color || (Color = {}));

Reverse mapping only works with numeric enums. String enums do not support it.

String Enums

String enums give each member an explicit string value instead of an auto-incrementing number. They read the same way in code, but they are far more readable in logs and debug output:

typescripttypescript
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}
 
console.log(Direction.Up); // "UP"

Every member must have a value. You cannot mix auto-incrementing numbers with strings in the same enum. TypeScript requires all members to be consistently either numeric or string-valued.

String enums are easier to debug because the runtime value is descriptive. Compare the logged output for each kind:

typescripttypescript
enum NumericStatus { Active, Inactive }
enum StringStatus { Active = "ACTIVE", Inactive = "INACTIVE" }
 
console.log(NumericStatus.Active); // 0 (not meaningful in logs)
console.log(StringStatus.Active);  // "ACTIVE" (immediately understandable)

A function parameter typed as a string enum also prevents accidental assignment of unrelated strings, even ones that look plausible at a glance. The compiler treats the enum member and its matching string literal as interchangeable, but rejects anything that does not exactly match a defined member:

typescripttypescript
function setStatus(status: StringStatus) {
  console.log(status);
}
 
setStatus(StringStatus.Active); // ok
setStatus("ACTIVE");            // ok: the literal matches
setStatus("active");            // Error: '"active"' is not assignable
setStatus("UNKNOWN");           // Error: not a member

Const Enums

A const enum is removed entirely during compilation. Each usage is replaced with the literal value, producing zero runtime code:

typescripttypescript
const enum Size {
  Small = "sm",
  Medium = "md",
  Large = "lg",
}
 
const buttonSize = Size.Medium;

After compilation, the JavaScript output keeps only the final value and drops every trace of the enum declaration that originally produced it:

javascriptjavascript
const buttonSize = "md";

The enum object is gone. Size.Medium was inlined to "md" during compilation. This is ideal for values that only exist as types and never need to be iterated or looked up at runtime.

Const enums do not work with every build tool. Bundlers that compile each file in isolation, such as esbuild, SWC, and most modern Vite or Next.js setups, cannot see across files to inline the value, so they run with the isolatedModules compiler flag and reject const enums outright. For more on compiler flags like this one, see strict mode in TypeScript. Check your build tool before relying on const enums.

A const enum cannot be used where a runtime object is expected. You cannot pass it to a function that expects an object, and you cannot use reverse mapping:

typescripttypescript
const enum Status { Active, Inactive }
 
console.log(Status[0]); // Error: const enums can only be used in property accesses

Enums vs Union Types

Many developers prefer union types over enums for new code. Both approaches let you define a set of allowed values, but they have different trade-offs.

typescripttypescript
// Enum approach
enum Role {
  Admin = "admin",
  Editor = "editor",
  Viewer = "viewer",
}
 
// Union type approach
type Role = "admin" | "editor" | "viewer";

The table below compares the two approaches:

EnumUnion Type
Runtime costRegular: generates object. Const: zero.Zero (type only)
Debug outputString enums are readable; numeric enums are numbersThe string itself
Reverse mappingNumeric enums onlyNot possible
Iterate over valuesYes (regular enums)No (type does not exist at runtime)
AutocompleteYes, after typing the enum nameYes, with string literal union
Extra import neededYesNo (just the type)

In general, prefer union types for new TypeScript code. They are simpler, produce no runtime output, and integrate naturally with the type system. Use enums when you need a runtime object that groups related constants, when reverse mapping is useful, or when you are working in an existing codebase that uses enums.

For more on union types, see union types in TypeScript.

Common Mistakes

Mixing Numeric and String Members

typescripttypescript
enum Mixed {
  Yes = "YES",
  No,
}

Once a member has an explicit string value, TypeScript can no longer auto-increment the next one the way it does in a purely numeric enum, so the member after it needs its own explicit value too:

texttext
Enum member must have an initializer.

Heterogeneous enums are technically legal in TypeScript as long as every member has an explicit value, but they are confusing to read and rarely useful. Keep enums consistently numeric or consistently string-valued.

Expecting Const Enums to Work Everywhere

typescripttypescript
const enum Colors { Red, Green, Blue }
 
// Error: const enums cannot be used as objects
Object.keys(Colors);

Const enums are inlined and do not exist at runtime. If you need to iterate over enum values, use a regular enum instead.

Using Numeric Enums for Public API Values

typescripttypescript
enum Status { Active, Inactive }
 
// API response: { "status": 0 }
// Was 0 Active or Inactive? The receiver cannot tell without the enum definition.

Numeric enums are not self-describing in JSON, logs, or API responses. Use string enums or union types when the value needs to be understood outside your TypeScript codebase.

Overusing Enums for Simple Constants

typescripttypescript
// Overkill: an enum for a single toggle
enum DebugMode { On, Off }

A simple boolean or a union of two strings would be simpler. Use enums when you have three or more related constants that belong together under one name.

Rune AI

Rune AI

Key Insights

  • An enum is a set of named constants grouped under one name.
  • Numeric enums auto-increment from 0 and support reverse mapping.
  • String enums produce readable values at runtime and are easier to debug.
  • Const enums are inlined at compile time and produce zero runtime code.
  • Prefer union types for most cases; use enums when you need a runtime value or reverse mapping.
RunePowered by Rune AI

Frequently Asked Questions

Should I use enums or union types in TypeScript?

Union types (`'a' | 'b' | 'c'`) are simpler, produce no extra JavaScript, and work naturally with TypeScript's type system. Use union types by default. Use enums when you need a runtime value with a descriptive name, when you need reverse mapping, or when migrating code that already uses enums.

Do TypeScript enums exist at runtime?

Yes. Unlike most TypeScript features, regular enums produce JavaScript objects at runtime. Each member becomes a property on the object. Const enums, however, are inlined at compile time and produce no runtime code.

Can I use computed values in an enum?

Yes, in numeric enums. You can use expressions and function calls as member values. However, string enums only accept string literals as values.

Conclusion

Enums give you a way to name a set of related constants so your code is more readable than raw numbers or strings. Numeric enums auto-increment, string enums are debuggable, and const enums disappear at compile time for zero runtime cost. For most new TypeScript code, union types are the simpler and more idiomatic choice. Reach for enums when you need a runtime object that groups related constants with meaningful names, or when you are working in a codebase that already uses them.