Skip to content

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

FixDevs ·

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:

  • const enum with isolatedModules — Vite, esbuild, and SWC use isolatedModules: true (each file transpiled independently). const enum requires cross-file type information that’s unavailable in this mode. Use regular enum or as const instead.
  • Enum erased at compile time with verbatimModuleSyntax — when TypeScript erases type-only imports, an enum import may be removed, causing runtime errors.
  • Numeric enum reverse mapping — numeric enums have unexpected behavior: enum Dir { Up = 0 } creates both Dir.Up === 0 and Dir[0] === "Up" at runtime. Comparing with raw numbers can cause confusion.
  • String enum case mismatchStatus.Active might compile to "Active" (capitalized) while your API returns "active" (lowercase). They’re not equal.
  • Re-exporting enums — re-exporting an enum from a barrel file with export type strips it (it becomes type-only), causing runtime errors.

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);  // ✓
paint('RED');      // ✓ — 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.

For related TypeScript issues, see Fix: TypeScript isolatedModules Error and Fix: TypeScript Property Does Not Exist on Type.

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