Skip to content

Fix: TypeScript Enum Not Working — const enum, isolatedModules, and Runtime Issues

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix TypeScript enum problems — const enum with isolatedModules, enums not available at runtime, string vs numeric enums, and migrating to union types or as const objects.

The Error

TypeScript enums fail in various ways. A const enum causes a build error:

error TS2748: Cannot access ambient const enums when 'isolatedModules' is enabled.

Or the enum value is undefined at runtime:

console.log(Direction.Up);  // undefined — enum not available in JavaScript output

Or an enum imported from another file doesn’t work:

error TS2469: The left-hand side of an arithmetic operation must be of type 'any',
'number', 'bigint' or an enum type.

Or a string enum comparison silently fails:

const status: Status = Status.Active;
status === 'active'  // false — should be true

Why This Happens

TypeScript enums have several behaviors that differ from other TypeScript features. They are one of the few TypeScript constructs that emit runtime JavaScript code rather than being erased during compilation. This makes them fundamentally different from interfaces, type aliases, and generics — which are purely compile-time constructs. The runtime emission creates friction with modern build tools that treat each file as an independent compilation unit.

The friction intensifies with const enum because it relies on cross-file type information. The TypeScript compiler inlines const enum values at every usage site, replacing Direction.Up with the literal 0 in the output. This only works if the compiler can see the enum definition while processing the file that uses it. Transpilers like esbuild, SWC, and Babel process each file in isolation (isolatedModules: true), so they have no way to look up the enum definition from another file.

Common triggers include: const enum with isolatedModules (Vite, esbuild, and SWC all use this mode), enum erased at compile time with verbatimModuleSyntax (TypeScript removes type-only imports, and an enum import may be removed), numeric enum reverse mapping (numeric enums create both Dir.Up === 0 and Dir[0] === "Up" at runtime), string enum case mismatch (Status.Active compiles to "Active" while your API returns "active"), and re-exporting enums from barrel files with export type which strips the runtime value.

How Other Languages Handle Enums

TypeScript’s enum is a source of controversy because it sits awkwardly between a type-level construct and a runtime value. Comparing it with enum implementations in other languages shows why the “don’t use enum” camp has a point — and why as const objects with derived union types often serve TypeScript projects better.

as const assertions (TypeScript itself) are the primary alternative. An as const object is plain JavaScript — no special emit, no cross-file inlining, no reverse mapping. The type is derived from the object using typeof Obj[keyof typeof Obj], producing a string literal union. The object works at runtime (Object.values(Status) to iterate), and the union type works at compile time. This is why the TypeScript team’s own documentation suggests as const for new projects. Discriminated unions go further: instead of a flat enum, you model each variant as a separate type with a shared kind field ({ kind: 'circle', radius: number } | { kind: 'square', side: number }). TypeScript narrows the type automatically in switch blocks and enforces exhaustive checking if you return from every branch.

Flow (Meta’s type checker for JavaScript) has its own enum implementation using flow enum. Flow enums are opaque by default — you can’t compare a Flow enum member to a raw string without explicit casting. This prevents the case-mismatch bugs that plague TypeScript string enums. Flow enums also don’t have reverse mapping or numeric auto-incrementing. The trade-off is that Flow has a much smaller ecosystem than TypeScript today.

Rust enums are algebraic data types. Each variant can carry different data: enum Shape { Circle(f64), Rectangle(f64, f64), None }. Pattern matching with match is exhaustive — the compiler refuses to compile if you don’t handle every variant. Rust enums have no runtime overhead beyond the discriminant tag. TypeScript’s discriminated unions are the closest analog, but Rust enforces exhaustive matching at compile time while TypeScript only warns via never checks if you set them up manually.

Go uses iota for constants that serve as enums: const ( Up = iota; Down; Left; Right ). There’s no enum type — these are untyped or typed integer constants. Go’s type system doesn’t enforce exhaustive switch handling, and there’s no built-in way to iterate over all values. It’s the simplest approach and the least safe. Python’s enum.Enum creates a class with member iteration, name-value lookup, and prevention of duplicate values. Python enums are runtime objects like TypeScript’s regular enums, but they’re more featureful — @unique prevents aliases, auto() generates values, and functional syntax allows dynamic creation.

The takeaway: TypeScript’s enum sits in an awkward middle ground. It emits runtime code like Python’s Enum but lacks Rust’s compile-time exhaustive matching. For most TypeScript projects, as const objects plus derived union types give you the runtime value, the type safety, and zero transpiler friction.

Fix 1: Replace const enum with Regular enum

const enum is inlined at compile time by the TypeScript compiler, but transpilers like esbuild and SWC that enable isolatedModules can’t inline across files:

// FAILS with isolatedModules (Vite, esbuild, SWC)
const enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT',
}

// WORKS — regular enum is emitted as JavaScript object
enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT',
}

Or configure TypeScript to use const enum safely:

// tsconfig.json
{
  "compilerOptions": {
    "isolatedModules": true,
    "verbatimModuleSyntax": true
  }
}

With these settings, TypeScript itself will warn you when const enum usage is unsafe — catch errors at type-check time rather than at runtime.

Fix 2: Use as const Object Instead of enum

The recommended modern approach is to use an as const object — it has no runtime surprises, works with every transpiler, and is more flexible:

// Instead of enum
enum Status {
  Active = 'active',
  Inactive = 'inactive',
  Pending = 'pending',
}

// Use as const object
const Status = {
  Active: 'active',
  Inactive: 'inactive',
  Pending: 'pending',
} as const;

// Derive the union type from the values
type Status = typeof Status[keyof typeof Status];
// type Status = "active" | "inactive" | "pending"

// Usage is identical
const userStatus: Status = Status.Active;  // "active"
const userStatus2: Status = 'active';      // Also valid — same type

Benefits over enum:

  • Works with all transpilers — no isolatedModules issues
  • No reverse mapping surprises
  • The derived type is a union type, directly compatible with string comparisons
  • Can use Object.values(Status) to get all values at runtime
  • Easier to serialize/deserialize from APIs

Fix 3: Fix String vs Numeric Enum Comparisons

Numeric enums compile with reverse mapping, which causes unexpected behavior:

enum Direction {
  Up,    // 0
  Down,  // 1
  Left,  // 2
  Right, // 3
}

// The compiled JavaScript object has BOTH directions:
// { 0: 'Up', 1: 'Down', Up: 0, Down: 1, Left: 2, Right: 3 }

Direction.Up === 0        // true
Direction[0] === 'Up'     // true — reverse mapping

// Pitfall: iterating over enum includes the reverse mappings
Object.keys(Direction)    // ['0', '1', '2', '3', 'Up', 'Down', 'Left', 'Right']
Object.values(Direction)  // [0, 1, 2, 3, 'Up', 'Down', 'Left', 'Right']

Always prefer string enums to avoid reverse mapping confusion:

enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT',
}

// No reverse mapping — clean object
// { Up: 'UP', Down: 'DOWN', Left: 'LEFT', Right: 'RIGHT' }

Object.keys(Direction)    // ['Up', 'Down', 'Left', 'Right']
Object.values(Direction)  // ['UP', 'DOWN', 'LEFT', 'RIGHT']

Fix case mismatch between enum and API:

// API returns lowercase: "active"
// Your enum has: Status.Active = "Active" (capital)
enum Status {
  Active = 'Active',  // Mismatch — API returns "active"
}

// Fix — match the API's casing
enum Status {
  Active = 'active',
  Inactive = 'inactive',
}

// Or normalize incoming data
function parseStatus(raw: string): Status {
  const lower = raw.toLowerCase();
  if (Object.values(Status).includes(lower as Status)) {
    return lower as Status;
  }
  throw new Error(`Unknown status: ${raw}`);
}

Fix 4: Fix Enum Re-exports in Barrel Files

When using barrel files (index.ts) that re-export everything, enums can be accidentally exported as type-only:

// components/index.ts — BROKEN with verbatimModuleSyntax
export type { Status } from './types';  // 'export type' strips runtime value

// components/index.ts — CORRECT
export { Status } from './types';       // Preserves the runtime object

The problem with export * from:

// If TypeScript is unsure whether Status is a type or value, it might
// treat it as type-only in certain configurations

// Explicit is safer
export { Status, type StatusType } from './types';
// Status = runtime enum object
// StatusType = compile-time type only

Check verbatimModuleSyntax behavior — with this setting, export type is enforced for type-only exports:

// tsconfig.json
{
  "compilerOptions": {
    "verbatimModuleSyntax": true
  }
}

// Now TypeScript enforces the distinction:
export type { MyInterface } from './types';  // Type only
export { MyEnum } from './types';            // Value (runtime)

Fix 5: Use Enums for Function Parameters Correctly

Enum values must be used via the enum reference, not as raw strings:

enum Color {
  Red = 'RED',
  Blue = 'BLUE',
}

function paint(color: Color): void {
  console.log(`Painting with ${color}`);
}

// CORRECT
paint(Color.Red);

// TypeScript ERROR — 'RED' is not assignable to type 'Color'
paint('RED');

// WORKAROUND — if you must accept string input
function paintFromString(color: string): void {
  if (!Object.values(Color).includes(color as Color)) {
    throw new Error(`Invalid color: ${color}`);
  }
  paint(color as Color);
}

This is why as const union types are more flexible:

const Color = { Red: 'RED', Blue: 'BLUE' } as const;
type Color = typeof Color[keyof typeof Color];

function paint(color: Color): void {}

paint(Color.Red);  // works
paint('RED');      // works — literal 'RED' is assignable to type Color

Fix 6: Handle Enum Serialization and Deserialization

Enums often cause issues at API boundaries — serializing to JSON and parsing back:

enum Priority {
  Low = 1,
  Medium = 2,
  High = 3,
}

interface Task {
  title: string;
  priority: Priority;
}

// Serialization — numeric enum serializes as a number
const task: Task = { title: 'Fix bug', priority: Priority.High };
JSON.stringify(task);
// {"title":"Fix bug","priority":3}  ← number, not "High"

// Deserialization — must validate the number is a valid enum value
function parseTask(raw: unknown): Task {
  const obj = raw as any;
  if (!Object.values(Priority).includes(obj.priority)) {
    throw new Error(`Invalid priority: ${obj.priority}`);
  }
  return { title: obj.title, priority: obj.priority as Priority };
}

String enums serialize and deserialize more naturally:

enum Priority {
  Low = 'LOW',
  Medium = 'MEDIUM',
  High = 'HIGH',
}

JSON.stringify({ priority: Priority.High });
// {"priority":"HIGH"}  ← readable string

// Parse back
const value = 'HIGH';
const priority = value as Priority;  // "HIGH" is assignable to Priority

Fix 7: Migrate from enum to Union Types Gradually

If enums are causing problems across your codebase, migrate incrementally:

Step 1 — add the as const object alongside the enum:

// Keep the enum temporarily for compatibility
enum Status {
  Active = 'active',
  Inactive = 'inactive',
}

// Add the new pattern
const StatusValues = {
  Active: 'active',
  Inactive: 'inactive',
} as const;
type StatusType = typeof StatusValues[keyof typeof StatusValues];

Step 2 — update new code to use the union type:

// New functions use the union type
function updateStatus(userId: string, status: StatusType): void {}

Step 3 — remove the enum when all usages are migrated.

Still Not Working?

Check if the enum is being tree-shaken. Bundlers may remove enum objects they think are unused if you only import the type:

// If you only use Status as a type and never reference Status.Active,
// bundlers might remove the Status object entirely
import { Status } from './types';

function check(s: Status) {}  // Type use only

Add a value usage to force the bundler to include it, or use sideEffects: false carefully in package.json.

Check for ambient enums in .d.ts files. If an enum is declared in a .d.ts declaration file (ambient context) and you’re using it as a value, it only exists as a type — there’s no runtime object to reference. This requires the original module to be imported to get the runtime value.

Verify TypeScript version. Enum behavior has changed across TypeScript versions. TypeScript 5.0 introduced changes to enum assignability. Run npx tsc --version and check the release notes for your version.

Watch for enums in monorepo packages. When a shared package exports an enum and the consuming app uses a different transpiler (e.g., the package is compiled with tsc but the app uses esbuild), the enum’s JavaScript output format may not match what the consuming transpiler expects. Build the shared package with tsc to emit a .js file alongside the .d.ts, and ensure the consuming app imports the compiled output rather than the raw TypeScript source.

Check ESLint rules that ban enums. Some ESLint configs include no-restricted-syntax rules that ban enum declarations entirely (e.g., @typescript-eslint/no-enum). If your linter is blocking enum usage, that’s intentional — the project has standardized on as const objects. Follow the project convention rather than disabling the rule.

For related TypeScript issues, see Fix: TypeScript isolatedModules Error, Fix: TypeScript Property Does Not Exist on Type, Fix: TypeScript Cannot Find Module, and Fix: TypeScript Strict Null Checks Error.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles