Question

How to Create Immutable Enum-Like Objects in JavaScript

Question

In JavaScript, how can I define a set of enum-like values so that the definition does not change later?

For example:

my.namespace.ColorEnum = {
  RED: 0,
  GREEN: 1,
  BLUE: 2
};

// later
if (currentColor == my.namespace.ColorEnum.RED) {
  // whatever
}

Is this enough to give the object the important characteristics of an enum in JavaScript, or is there a better way to define it so the values cannot be modified accidentally?

Short Answer

By the end of this page, you will understand how JavaScript handles enum-like patterns, why plain objects are not truly protected by default, and how to make them safer with Object.freeze(). You will also learn common enum patterns, how developers use them in real projects, and what mistakes to avoid when comparing or mutating enum-like values.

Concept

JavaScript does not have traditional enums in older standard syntax the way some languages like Java or C# do. Instead, developers often create enum-like objects: plain objects that group a fixed set of named values.

A basic example is:

const Color = {
  RED: 0,
  GREEN: 1,
  BLUE: 2
};

This works well for readability because Color.RED is clearer than writing 0 directly in your code.

However, a plain object is still mutable by default. That means someone could do this later:

Color.RED = 99;
Color.YELLOW = 3;

That is the main problem: the object behaves like a container of constants, but JavaScript does not automatically protect it.

To make an enum-like object safer, developers usually use Object.freeze():

const Color = Object.freeze({
  RED: 0,
  GREEN: 1,
  BLUE: 2
});

Object.freeze() prevents:

  • adding new properties
  • removing properties
  • changing existing properties

This makes the object much closer to the idea of an enum.

Why this matters:

  • It prevents accidental bugs.
  • It makes shared constants predictable.
  • It improves readability across a codebase.
  • It helps teams standardize values like states, roles, status codes, or categories.

In modern JavaScript and TypeScript, there are additional patterns, but for plain JavaScript, a frozen object is one of the most common and practical solutions.

Mental Model

Think of an enum-like object as a printed legend on a map.

  • RED, GREEN, and BLUE are the labels.
  • 0, 1, and 2 are the official meanings.

If the legend can be edited at any time, the map becomes unreliable. Someone could change RED from 0 to 99, and now everyone reading the map may interpret it differently.

Using Object.freeze() is like laminating that legend so nobody can rewrite it by accident.

So the mental model is:

  • a plain object = a writable label sheet
  • a frozen object = a fixed set of official labels
Take Quiz

Syntax and Examples

The most common enum-like syntax in JavaScript is a frozen object.

const Color = Object.freeze({
  RED: 0,
  GREEN: 1,
  BLUE: 2
});

let currentColor = Color.RED;

if (currentColor === Color.RED) {
  console.log("The color is red");
}

Why this is better

  • const prevents the variable Color from being reassigned.
  • Object.freeze() prevents the properties inside the object from being changed.
  • === is preferred over == for exact comparison.

String-based enums

In many JavaScript projects, strings are easier to debug than numbers:

const Color = Object.freeze({
  RED: "RED",
  GREEN: "GREEN",
  BLUE: "BLUE"
});

let currentColor = Color.GREEN;

if (currentColor === Color.GREEN) {
  console.log("The color is green");
}

This can be easier to read in logs, APIs, and JSON data.

Namespaced version

If you are using a namespace-like object:

const my = {
  namespace: {}
};

my.namespace.ColorEnum = Object.freeze({
  RED: 0,
  GREEN: 1,
  BLUE: 2
});

This preserves your original style but makes the enum-like object immutable.

Step by Step Execution

Consider this example:

const Status = Object.freeze({
  PENDING: 0,
  APPROVED: 1,
  REJECTED: 2
});

let currentStatus = Status.APPROVED;

if (currentStatus === Status.APPROVED) {
  console.log("Approved request");
}

Here is what happens step by step:

  1. Object.freeze({...}) creates an object with three properties.
  2. The object is frozen, so its properties cannot be changed, deleted, or extended.
  3. currentStatus is assigned the value of Status.APPROVED, which is 1.
  4. The if statement compares currentStatus with Status.APPROVED using ===.
  5. Since both values are exactly 1, the condition is true.
  6. The message Approved request is printed.

Now imagine someone tries this:

Status.APPROVED = 99;
console.log(Status.APPROVED);

In non-strict mode, the change fails silently and the value stays 1. In strict mode, JavaScript throws an error.

That protection is the key benefit of freezing the object.

Real World Use Cases

Enum-like objects are common in real applications whenever you have a small fixed set of allowed values.

Application states

const RequestState = Object.freeze({
  IDLE: "IDLE",
  LOADING: "LOADING",
  SUCCESS: "SUCCESS",
  ERROR: "ERROR"
});

Used in UI code to track loading status.

User roles

const UserRole = Object.freeze({
  ADMIN: "ADMIN",
  EDITOR: "EDITOR",
  VIEWER: "VIEWER"
});

Used in permission checks.

API result types

const ResultType = Object.freeze({
  JSON: "json",
  TEXT: "text",
  BLOB: "blob"
});

Used to decide how to parse a response.

Order or payment status

const PaymentStatus = Object.freeze({
  PENDING: "pending",
  PAID: "paid",
  FAILED: "failed"
});

Used in e-commerce systems and backend logic.

Logging levels

const LogLevel = Object.freeze({
  INFO: "info",
  WARN: "warn",
  ERROR: "error"
});

Used to standardize logs across services and scripts.

Take Quiz

Real Codebase Usage

In real codebases, enum-like objects are usually not just stored and compared. They are used as part of broader patterns.

Centralized constants

Teams often keep enums in one file so all modules use the same values:

export const OrderStatus = Object.freeze({
  PENDING: "PENDING",
  SHIPPED: "SHIPPED",
  DELIVERED: "DELIVERED"
});

This avoids duplicated string literals across the codebase.

Guard clauses and validation

Developers often validate input against enum values:

const Role = Object.freeze({
  ADMIN: "ADMIN",
  USER: "USER"
});

function isValidRole(role) {
  return Object.values(Role).includes(role);
}

function createUser(name, role) {
  if (!isValidRole(role)) {
    throw new Error("Invalid role");
  }

  return { name, role };
}

Switch statements

Enums are often used with switch when behavior depends on one of several named values:

function getLabel(status) {
  switch (status) {
    case "PENDING":
      return "Waiting";
    case "APPROVED":
      return "Accepted";
    case "REJECTED":
      return "Declined";
    default:
      return "Unknown";
  }
}

A cleaner version uses the enum object itself.

Configuration and feature flags

Fixed values are also used in config files and application modes:

const Environment = Object.freeze({
  DEVELOPMENT: "development",
  PRODUCTION: "production",
  TEST: "test"
});

Error handling

Enums help standardize error codes or categories:

const ErrorCode = Object.freeze({
  NOT_FOUND: "NOT_FOUND",
  UNAUTHORIZED: "UNAUTHORIZED"
});

This makes condition handling more consistent than raw strings scattered throughout the project.

Common Mistakes

1. Using a plain object and assuming it is protected

Broken example:

const Color = {
  RED: 0,
  GREEN: 1,
  BLUE: 2
};

Color.RED = 99;

Problem: The value can be changed.

Fix:

const Color = Object.freeze({
  RED: 0,
  GREEN: 1,
  BLUE: 2
});

2. Using == instead of ===

Broken example:

if (currentColor == Color.RED) {
  // ...
}

Problem: == allows type coercion, which can lead to surprising matches.

Fix:

if (currentColor === Color.RED) {
  // ...
}

3. Thinking const makes object properties immutable

Broken example:

const Color = {
  RED: 0
};

Color.RED = 5;

Problem: const protects the variable binding, not the object's internal properties.

Fix: Use Object.freeze() if you want the properties protected.

4. Mixing numeric and string values carelessly

Broken example:

const Status = Object.freeze({
  OK: 1,
  FAIL: "FAIL"
});

Problem: Mixed value types can make comparisons and debugging harder.

Fix: Choose one style consistently unless you have a clear reason not to.

5. Assuming Object.freeze() is deeply recursive

Broken example:

const Config = Object.freeze({
  colors: {
    primary: "blue"
  }
});

Config.colors.primary = "red";

Problem: The top-level object is frozen, but nested objects are still mutable.

Fix: Freeze nested objects too if needed, or use a deep-freeze helper.

6. Hardcoding raw strings everywhere

Broken example:

if (status === "PENDNG") {
  console.log("Waiting");
}

Problem: Typos are easy to miss.

Fix:

const Status = Object.freeze({
  PENDING: "PENDING"
});

if (status === Status.PENDING) {
  console.log("Waiting");
}

Comparisons

ApproachExampleMutable?Good forNotes
Plain object{ RED: 0 }YesSmall quick scriptsSimple, but not protected
Frozen objectObject.freeze({ RED: 0 })No, at top levelMost JavaScript projectsCommon enum-like pattern
String enum-like values{ RED: "RED" }Depends on freezeReadability and APIsEasier to debug than numbers
Numeric enum-like values{ RED: 0 }Depends on freezeCompact internal valuesLess descriptive in logs
const onlyconst Color = {...}Properties still mutablePreventing reassignment onlyNot enough for enum safety

Strings vs numbers

StyleExampleProsCons
NumbersRED: 0Compact, familiar from other languagesHarder to read in logs and JSON
StringsRED: "RED"Self-describing, easier to debugSlightly more verbose

In many JavaScript applications, string enums are easier to maintain unless numeric values are specifically required.

Take Quiz

Cheat Sheet

// Basic enum-like object
const Color = Object.freeze({
  RED: "RED",
  GREEN: "GREEN",
  BLUE: "BLUE"
});

// Usage
if (currentColor === Color.RED) {
  console.log("red");
}

Rules

  • JavaScript objects are mutable by default.
  • const does not freeze object properties.
  • Object.freeze() makes the top-level object immutable.
  • Use === for comparisons.
  • Prefer strings when readability matters.
  • Prefer one consistent value type.

Quick checks

Get all enum values:

Object.values(Color)

Validate a value:

Object.values(Color).includes(currentColor)

Important edge case

Object.freeze() is shallow:

const Example = Object.freeze({
  nested: { x: 1 }
});

Example.nested.x = 2; // still possible

Safe pattern

const Status = Object.freeze({
  PENDING: "PENDING",
  DONE: "DONE"
});

Use enum-like objects for:

  • statuses
  • roles
  • modes
  • categories
  • state names

FAQ

Does JavaScript have real enums?

Not in older plain JavaScript syntax. The common approach is to use an object as an enum-like container, usually with Object.freeze().

Is const enough to make an enum immutable?

No. const only prevents reassignment of the variable itself. The properties of the object can still change unless you freeze it.

Should I use numbers or strings for enum values in JavaScript?

Strings are often easier to read and debug. Numbers are fine if you specifically need numeric values.

Why use Object.freeze() for enums?

It prevents accidental modification, deletion, or extension of the enum-like object.

Can Object.freeze() protect nested objects too?

No. It only freezes the top level. Nested objects remain mutable unless they are frozen separately.

Is == okay when comparing enum values?

It is better to use === to avoid type coercion and unexpected matches.

How do I check if a value is part of an enum-like object?

Use Object.values():

Object.values(Color).includes(value)

Is a frozen object the standard enum pattern in JavaScript?

Yes, it is one of the most common patterns in plain JavaScript when you want a fixed set of named constants.

Take Quiz

Mini Project

Description

Build a small order status module for an application. The project demonstrates how to define a fixed set of allowed status values, protect them from accidental changes, validate incoming data, and use the values in control flow.

Goal

Create and use an immutable enum-like object for order statuses in JavaScript.

Requirements

  1. Define an enum-like object for order statuses.
  2. Prevent the status definitions from being modified.
  3. Write a function that checks whether a given status is valid.
  4. Write a function that returns a human-readable label for each status.
  5. Demonstrate usage with both valid and invalid inputs.
Take Quiz

Keep learning

Related questions

View more