Fix: TypeScript Enum Not Working — const enum, isolatedModules, and Runtime Issues
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 outputOr 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 trueWhy This Happens
TypeScript enums have several behaviors that differ from other TypeScript features:
const enumwithisolatedModules— Vite, esbuild, and SWC useisolatedModules: true(each file transpiled independently).const enumrequires cross-file type information that’s unavailable in this mode. Use regularenumoras constinstead.- 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 bothDir.Up === 0andDir[0] === "Up"at runtime. Comparing with raw numbers can cause confusion. - String enum case mismatch —
Status.Activemight 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 typestrips 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 typeBenefits over enum:
- Works with all transpilers — no
isolatedModulesissues - 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 objectThe 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 onlyCheck 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 ColorFix 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 PriorityFix 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 onlyAdd 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.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: pdf-lib Not Working — PDF Not Generating, Fonts Not Embedding, or Pages Blank
How to fix pdf-lib issues — creating PDFs from scratch, modifying existing PDFs, embedding fonts and images, form filling, merging documents, and browser and Node.js usage.
Fix: Pusher Not Working — Events Not Received, Channel Auth Failing, or Connection Dropping
How to fix Pusher real-time issues — client and server setup, channel types, presence channels, authentication endpoints, event binding, connection management, and React integration.
Fix: date-fns Not Working — Wrong Timezone Output, Invalid Date, or Locale Not Applied
How to fix date-fns issues — timezone handling with date-fns-tz, parseISO vs new Date, locale import and configuration, DST edge cases, v3 ESM migration, and common format pattern mistakes.
Fix: Zod Validation Not Working — safeParse Returns Wrong Error, transform Breaks Type, or discriminatedUnion Fails
How to fix Zod schema validation issues — parse vs safeParse, transform and preprocess, refine for cross-field validation, discriminatedUnion, error formatting, and common schema mistakes.