Skip to editor content
learningtypescript.orglesson 16 of 25

Enums And Constants

Hardcoded strings and magic numbers are a maintenance nightmare. What does status === 2 mean? Is "ADMIN" spelled correctly everywhere? TypeScript gives you two powerful tools to solve this: enums and const assertions. Both let you define a fixed set of named values that are easy to read, impossible to misspell, and fully type-checked.

Numeric Enums

The simplest enum assigns auto-incrementing numbers to each member, starting at zero. This is useful when you care about ordering or need to compare values, but the raw number doesn't need to mean anything to a human.

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

function move(direction: Direction): string {
  switch (direction) {
    case Direction.Up:    return "Moving up!";
    case Direction.Down:  return "Moving down!";
    case Direction.Left:  return "Moving left!";
    case Direction.Right: return "Moving right!";
  }
}

console.log(move(Direction.Up));    // "Moving up!"
console.log(Direction.Up);          // 0
console.log(Direction[0]);          // "Up"  ← reverse mapping

// You can also set custom starting values
enum HttpStatus {
  OK = 200,
  Created = 201,
  BadRequest = 400,
  Unauthorized = 401,
  NotFound = 404,
  InternalServerError = 500,
}

console.log(HttpStatus.OK);         // 200
console.log(HttpStatus.NotFound);   // 404

Notice the reverse mapping on line 12: numeric enums let you look up a name by its number (Direction[0] returns "Up"). This can be handy for debugging, but it also means the compiled JavaScript includes extra code.

String Enums

String enums are more common in real-world TypeScript because they produce readable values at runtime. When you log a string enum to the console or send it over an API, you see "ACTIVE" rather than 1.

enum OrderStatus {
  Pending   = "PENDING",
  Confirmed = "CONFIRMED",
  Shipped   = "SHIPPED",
  Delivered = "DELIVERED",
  Cancelled = "CANCELLED",
}

interface Order {
  id: number;
  item: string;
  status: OrderStatus;
}

function describeOrder(order: Order): string {
  switch (order.status) {
    case OrderStatus.Pending:
      return `Order #${order.id} is waiting to be confirmed.`;
    case OrderStatus.Confirmed:
      return `Order #${order.id} has been confirmed!`;
    case OrderStatus.Shipped:
      return `Order #${order.id} is on its way.`;
    case OrderStatus.Delivered:
      return `Order #${order.id} has been delivered. Enjoy!`;
    case OrderStatus.Cancelled:
      return `Order #${order.id} was cancelled.`;
  }
}

const order: Order = { id: 42, item: "TypeScript Handbook", status: OrderStatus.Shipped };
console.log(describeOrder(order));
// "Order #42 is on its way."

console.log(order.status);
// "SHIPPED"  ← readable at runtime

Unlike numeric enums, string enums do not have reverse mappings, which results in smaller compiled output. For most applications, string enums are the safer, more readable choice.

Const Assertions

Sometimes you want a plain object to behave like an enum — immutable values with type inference. The as const assertion freezes an object or array so TypeScript treats every value as a literal type instead of a general string or number.

const Theme = {
  Light:  "light",
  Dark:   "dark",
  System: "system",
} as const;

// Extract the union of all value types
type ThemeMode = typeof Theme[keyof typeof Theme];
// ThemeMode = "light" | "dark" | "system"

function applyTheme(mode: ThemeMode): void {
  console.log(`Applying theme: ${mode}`);
}

applyTheme(Theme.Dark);    // OK
applyTheme("light");       // Also OK — literal matches
// applyTheme("purple");   // Error: not assignable to ThemeMode

// Works on arrays too
const SUPPORTED_LOCALES = ["en", "fr", "de", "ja"] as const;
type Locale = typeof SUPPORTED_LOCALES[number];
// Locale = "en" | "fr" | "de" | "ja"

const userLocale: Locale = "fr";
console.log(`User locale: ${userLocale}`);
console.log(`Supported: ${SUPPORTED_LOCALES.join(", ")}`);

The pattern typeof Obj[keyof typeof Obj] is a common idiom in TypeScript for extracting a union type from a const object's values. Keep it in your toolbox.

Enums vs Const Assertions: When to Use Each

Both solve the same problem, but they have different tradeoffs.

FeatureEnumConst Object (as const)
Syntaxenum Foo { Bar }const Foo = { Bar: "bar" } as const
Runtime valueYesYes
IterableAwkwardObject.values(Foo) works
Reverse mappingNumeric onlyNo
Tree-shakeableNoYes
ExtendableNoYes (spread)

A good rule of thumb: use const objects with as const by default because they integrate naturally with the rest of JavaScript. Reach for enums when you need the auto-incrementing behavior of numeric enums or want the explicit enum keyword to signal intent clearly.

// Prefer const objects for role-based access — easy to iterate
const Role = {
  Admin:  "ADMIN",
  Editor: "EDITOR",
  Viewer: "VIEWER",
} as const;

type RoleType = typeof Role[keyof typeof Role];

function checkPermission(role: RoleType, action: string): boolean {
  if (role === Role.Admin) return true;
  if (role === Role.Editor && action !== "delete") return true;
  return false;
}

// Easily list all roles
const allRoles = Object.values(Role);
console.log("Roles:", allRoles);

console.log(checkPermission(Role.Admin, "delete"));   // true
console.log(checkPermission(Role.Editor, "delete"));  // false
console.log(checkPermission(Role.Editor, "edit"));    // true
console.log(checkPermission(Role.Viewer, "view"));    // false

Try It Yourself

Implement a traffic light system using an enum for light states and a const object for timing configuration. The simulation should cycle through six light changes and print a description of each state.

enum TrafficLight {
  Red    = "RED",
  Yellow = "YELLOW",
  Green  = "GREEN",
}

const LIGHT_DURATION: Record<TrafficLight, number> = {
  [TrafficLight.Red]:    30,
  [TrafficLight.Yellow]: 5,
  [TrafficLight.Green]:  25,
};

function getNextLight(current: TrafficLight): TrafficLight {
  switch (current) {
    case TrafficLight.Red:    return TrafficLight.Green;
    case TrafficLight.Green:  return TrafficLight.Yellow;
    case TrafficLight.Yellow: return TrafficLight.Red;
  }
}

function describeLight(light: TrafficLight): string {
  const duration = LIGHT_DURATION[light];
  switch (light) {
    case TrafficLight.Red:
      return `STOP    — Red light    (${duration}s)`;
    case TrafficLight.Yellow:
      return `CAUTION — Yellow light (${duration}s)`;
    case TrafficLight.Green:
      return `GO      — Green light  (${duration}s)`;
  }
}

// Simulate six light changes starting from Red
let current = TrafficLight.Red;
for (let i = 0; i < 6; i++) {
  console.log(describeLight(current));
  current = getNextLight(current);
}

Try extending this: add a PedestrianLight enum with Walk and Stop states, and make it toggle whenever the traffic light changes to Red.

Key Takeaways

  • Numeric enums auto-increment from zero and support reverse mapping (Enum[value] gives the name), but produce more runtime code.
  • String enums use explicit string values, are readable at runtime, and are the most common choice in TypeScript codebases.
  • as const freezes an object so TypeScript infers literal types rather than general ones, enabling the same type-safety as enums with plain JavaScript objects.
  • Use typeof Obj[keyof typeof Obj] to extract a union type of all values from a const object.
  • Const objects are easier to iterate with Object.values() and are tree-shakeable — prefer them by default and reach for enums when their specific features (auto-increment, explicit enum syntax) add clarity.
  • Both enums and const assertions eliminate magic strings and numbers from your code, making refactoring safer and intent clearer.

Pro Tip: Avoid mixing numeric and string members in a single enum (heterogeneous enums). TypeScript allows it, but it creates confusing runtime behavior and offers no practical benefit. Stick to all-numeric or all-string enums, or switch to a const object instead.

Next Steps

Enums and const assertions give you fixed sets of values — but how does TypeScript figure out which specific member of a union you're working with at any given point? Next, you'll learn type narrowing: the mechanism TypeScript uses to refine broad types into specific ones through typeof, instanceof, discriminated unions, and custom type guards.

Next lesson

Type Narrowing

Learn TypeScript type narrowing with typeof, instanceof, in operator, discriminated unions, and user-defined type guards.

25 min