← Back to Home

JavaScript & TypeScript Enum Guide

Front-end · Fullstack

Complete JavaScript & TypeScript Enum Guide

TypeScript introduces enum features to JavaScript, providing multiple approaches including `enum`, `const enum`, and `as const`. Each approach has different compilation results and use cases. This guide will help you choose the right enum pattern and seamlessly integrate with tools like Zod, JSON Schema, OpenAPI, etc.

This guide covers standard enums, compile-time constants, object literals, runtime checks, and integration with validation libraries and API documentation.

TypeScript Enums: enum, const enum & as const

TypeScript enums come in three main forms: standard `enum` generates runtime code, `const enum` is inlined at compile time, and `as const` is a pure type-level constraint. Understanding their differences is important for optimizing bundle size and maintaining type safety.

Common Templates

// Standard enum
export enum OrderStatus {
  Pending = 'pending',
  Paid = 'paid',
  Refunded = 'refunded',
}

// const enum (compile-time inline)
export const enum PaymentMethod {
  Card = 'card',
  Paypal = 'paypal',
}

// as const object
export const Roles = {
  Admin: 'admin',
  Editor: 'editor',
  Viewer: 'viewer',
} as const;
export type Role = typeof Roles[keyof typeof Roles];

Each approach has trade-offs: enum is most flexible but larger in size, const enum is lightest but has compatibility issues, as const is simplest but limited in functionality.

Enum Patterns in JavaScript

JavaScript itself doesn't have enum types, but we can use `Object.freeze()` to create immutable objects to simulate them. This is common in projects that don't use TypeScript.

Object.freeze Enum

const Status = Object.freeze({
  PENDING: 'pending',
  PAID: 'paid',
  REFUNDED: 'refunded',
});

function isStatus(value) {
  return Object.values(Status).includes(value);
}

Also covers frozen objects and validation functions in pure JavaScript environments to implement enum-like constraints.

Framework Integration

In frontend frameworks like React, Vue, Next.js, enums are commonly used for component props, state management, and route definitions. TypeScript's type checking ensures no incorrect values are passed.

Whether Angular, Vue, or React, enums make component interfaces clearer and type-safe.

Schema & Validation

Frontend enums often need to stay consistent with backend APIs, database schemas, and validation libraries. Tools like Zod, JSON Schema, OpenAPI all provide enum support.

Zod & tRPC

const StatusEnum = z.enum(['pending', 'paid', 'refunded']);

type Status = z.infer;

router.mutation('updateOrder', {
  input: z.object({ status: StatusEnum }),
  resolve({ input }) {
    /* ... */
  }
});

Zod's enum method provides both runtime validation and automatic TypeScript type inference, making it ideal for full-stack projects.

OpenAPI & JSON Schema

const schema = {
  type: 'string',
  enum: Object.values(OrderStatus),
  description: 'Order status code',
  examples: ['pending']
};

Explicitly listing enum values in API documentation helps frontend developers clearly understand available options.

Database & ORM

When using Prisma or TypeORM, you can automatically generate TypeScript types from enums in database schemas, avoiding the hassle of manual synchronization.

This way, frontend, backend, and database all use the same enum definition, reducing the chance of errors.

TypeScript Enum Playbook

TypeScript Enum Cheat Sheet

  • • Complete examples: enum, const enum, as const, string enums
  • • Zod / Yup validation integration
  • • Naming conventions: PascalCase enum types, UPPER_CASE constant values

Team Code Review Checklist

✓ Type Selection

  • • Need runtime object: use enum
  • • Only need compile-time type: prefer as const
  • • Need zero runtime overhead: use const enum (note limitations)
  • • Integration with backend: consider using Zod schema for runtime validation

✓ Serialization

  • • String enums better than numeric enums (better API compatibility)
  • • Use const assertion to preserve literal types
  • • Keep JSON Schema / OpenAPI definitions in sync with code
  • • Consider using openapi-typescript to automatically generate types

✓ Best Practices

  • • Avoid mixing numeric and string enums
  • • Use Record<Enum, T> to ensure all cases are handled
  • • Run full type check when enum values change
  • • Consider sharing enum definitions between frontend and backend (monorepo)

Naming and Design Anti-patterns

❌ Anti-pattern: Using numeric enum

enum Status {
  Pending,      // 0
  Processing,   // 1
  Completed     // 2
}
// JSON: {"status": 1} - Not readable in frontend

Issue: API returns numbers, difficult to debug and understand

✅ Correct: Use string enum

enum Status {
  Pending = "pending",
  Processing = "processing",
  Completed = "completed"
}
// JSON: {"status": "pending"} - Clear and readable

❌ Anti-pattern: const enum doesn't exist at runtime

const enum Status {
  Pending = "pending"
}

// Cannot iterate or reflect
Object.values(Status) // Runtime error!

✅ Correct: Use as const or regular enum

const Status = {
  Pending: "pending",
  Processing: "processing",
  Completed: "completed"
} as const;

type Status = typeof Status[keyof typeof Status];

// Can iterate
Object.values(Status) // Works at runtime

❌ Anti-pattern: Incomplete switch

function handle(status: Status) {
  switch (status) {
    case Status.Pending:
      return "Pending";
    case Status.Processing:
      return "Processing";
    // Forgot Completed!
  }
}

✅ Correct: Use exhaustive check

function handle(status: Status): string {
  switch (status) {
    case Status.Pending:
      return "Pending";
    case Status.Processing:
      return "Processing";
    case Status.Completed:
      return "Completed";
    default:
      const _exhaustive: never = status;
      throw new Error(`Unhandled status: ${_exhaustive}`);
  }
}

Zod Validation Integration Example

Combining Zod for runtime type safety:

import { z } from "zod";

// 1. Define Zod schema
const OrderStatusSchema = z.enum([
  "pending",
  "processing", 
  "completed",
  "failed"
]);

// 2. Infer TypeScript type
type OrderStatus = z.infer<typeof OrderStatusSchema>;

// 3. Create validation function
function parseOrderStatus(input: unknown): OrderStatus {
  return OrderStatusSchema.parse(input);
}

// 4. Use in API handling
app.post("/api/orders", (req, res) => {
  try {
    const status = parseOrderStatus(req.body.status);
    // status is now type-safe
  } catch (error) {
    res.status(400).json({ 
      error: "Invalid status value" 
    });
  }
});

// 5. React Hook Form integration
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

const formSchema = z.object({
  status: OrderStatusSchema,
});

function OrderForm() {
  const form = useForm({
    resolver: zodResolver(formSchema),
  });
}

Tip: Zod provides both compile-time types and runtime validation

Jest/Vitest Test Template

Ensure enum completeness:

import { describe, test, expect } from "vitest";

const OrderStatus = {
  Pending: "pending",
  Processing: "processing",
  Completed: "completed",
} as const;

type OrderStatus = typeof OrderStatus[keyof typeof OrderStatus];

describe("OrderStatus", () => {
  test("all enum values are strings", () => {
    const values = Object.values(OrderStatus);
    values.forEach(value => {
      expect(typeof value).toBe("string");
    });
  });
  
  test("no duplicate values", () => {
    const values = Object.values(OrderStatus);
    const uniqueValues = new Set(values);
    expect(uniqueValues.size).toBe(values.length);
  });
  
  test("matches backend contract", () => {
    const expectedValues = ["pending", "processing", "completed"];
    const actualValues = Object.values(OrderStatus);
    expect(actualValues.sort()).toEqual(expectedValues.sort());
  });
  
  test.each(Object.values(OrderStatus))(
    "business logic handles status: %s",
    (status) => {
      const result = handleOrderStatus(status);
      expect(result).toBeDefined();
    }
  );
  
  test("type guard works correctly", () => {
    function isOrderStatus(value: unknown): value is OrderStatus {
      return Object.values(OrderStatus).includes(value as OrderStatus);
    }
    
    expect(isOrderStatus("pending")).toBe(true);
    expect(isOrderStatus("invalid")).toBe(false);
  });
});

Tip: Use test.each to automatically test all enum values

Frontend Framework Integration

Using Enums in React

const OrderStatus = {
  Pending: "pending",
  Processing: "processing",
  Completed: "completed",
} as const;

type OrderStatusType = typeof OrderStatus[keyof typeof OrderStatus];

interface OrderProps {
  status: OrderStatusType;
}

function OrderBadge({ status }: OrderProps) {
  const statusConfig: Record<OrderStatusType, { label: string; color: string }> = {
    [OrderStatus.Pending]: { label: "Pending", color: "yellow" },
    [OrderStatus.Processing]: { label: "Processing", color: "blue" },
    [OrderStatus.Completed]: { label: "Completed", color: "green" },
  };
  
  const config = statusConfig[status];
  return <Badge color={config.color}>{config.label}</Badge>;
}

Vue 3 Composition API

import { computed, ref } from 'vue';

const OrderStatus = {
  Pending: 'pending',
  Processing: 'processing',
  Completed: 'completed',
} as const;

type OrderStatus = typeof OrderStatus[keyof typeof OrderStatus];

const currentStatus = ref<OrderStatus>(OrderStatus.Pending);

const statusLabel = computed(() => {
  const labels: Record<OrderStatus, string> = {
    [OrderStatus.Pending]: 'Pending',
    [OrderStatus.Processing]: 'Processing',
    [OrderStatus.Completed]: 'Completed',
  };
  return labels[currentStatus.value];
});

Tip: Use Record type to ensure all enum values are covered

FAQ

TypeScript enum vs as const?

`enum` generates runtime code and is suitable for cross-file use; `as const` is lighter, works with `keyof` to get union types, suitable for pure types or Tree Shaking scenarios.

What are the pitfalls of const enum?

Build tools without `preserveConstEnums` enabled will erase them; need to ensure all consumers compile consistently.

How to display frontend enums in Swagger?

Ensure backend returns string values, list `enum` array in OpenAPI schema, and provide `description` and `examples`.

How to share enums between Prisma & TypeScript?

Use `prisma.$enum` to generate TypeScript types, or output `enum` definitions through generators to avoid duplicate maintenance.