Why TypeScript Enums Are Dead

Programming Best Practices

Node.js v22.6.0+ introduced type-stripping, allowing direct TS execution but breaking traditional enums. Learn why `as const` objects are the modern, future-proof alternative.

Node.js v22.6.0 introduced a significant change with its type-stripping support, enabling direct execution of TypeScript files. While this streamlines development, it fundamentally breaks traditional TypeScript enums. This article explores why as const objects are the modern, future-proof alternative, offering seamless compatibility with Node's new capabilities.

Node.js v22.6.0 marked a pivotal shift in how TypeScript is integrated. As of v22.18.0, type stripping is enabled by default, allowing you to execute .ts files directly using node file.ts without any special flags. For versions 22.6.0 through 22.17.0, the --experimental-strip-types flag is required. This functionality relies on your code utilizing erasable TypeScript syntax, where Node can simply remove type annotations at runtime without a build step. While this 'compile-less' approach feels transformative for most TypeScript features, traditional enums present a critical exception that disrupts this elegant workflow.

The Type-Stripping Revolution

Node's type-stripping capability integrates seamlessly with standard TypeScript syntax. Enabled by default since v22.18.0, it allows direct execution of code like the following using node example.ts:

function greet(name: string) {
  console.log(`Hello, ${name}!`);
}
greet('TypeScript');

Node efficiently strips away the type annotations, executing the underlying JavaScript. This eliminates the need for a separate TypeScript compiler, build configurations, or compilation waits, delivering the 'compile-less' TypeScript experience many developers have sought.

The Enum Problem

Traditional TypeScript enums pose a significant challenge to this workflow because they are not erasable syntax. Unlike simple type annotations, enums require TypeScript to generate actual runtime JavaScript objects. Consider this common example:

enum Color {
  Red,
  Green,
  Blue,
}
console.log(Color.Red);

Attempting to run this directly with Node using node color.ts will result in an error:

SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode

This error occurs because enums necessitate a code transformation process, not merely type erasure. Node's integrated type stripper is designed only for erasable syntax and cannot transform code that generates runtime objects. While a workaround exists via the --experimental-transform-types flag (available since v22.7.0), which implicitly enables type stripping and adds support for enums and namespaces, relying on experimental flags for production environments is generally discouraged due to potential behavioral changes in future versions. Adhering to erasable-only syntax ensures maximum compatibility and stability.

The Modern Alternative

Instead of working against Node's type-stripping architecture, you can achieve equivalent functionality and type safety using plain JavaScript objects combined with as const. This pattern offers identical clarity and type safety while integrating perfectly with Node's erasable-only approach:

const Color = {
  RED: 'red',
  GREEN: 'green',
  BLUE: 'blue',
} as const;

type Color = (typeof Color)[keyof typeof Color];

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

This as const pattern offers numerous benefits. It works seamlessly with Node's type stripping because it fundamentally relies on standard JavaScript augmented with TypeScript types. This means zero runtime overhead and no need for specialized compiler transformations. Developers maintain both robust runtime constants and compile-time type safety, and the pattern integrates well with modern tooling, including ESLint, bundlers, and type checkers.

When to Use Type Unions

For scenarios where a runtime object is unnecessary and you simply require a defined set of allowed string values, a more straightforward approach using type unions can be employed:

type Role = 'admin' | 'editor' | 'viewer';

This type union syntax is fully erasable by Node, resulting in zero runtime footprint. It is ideal for situations demanding compile-time type checking without the need for corresponding runtime constants.

Catching Errors at Compile Time

TypeScript 5.8 introduced the --erasableSyntaxOnly compiler flag, a valuable tool for proactively identifying compatibility issues. When this flag is enabled, TypeScript strictly enforces the use of only erasable constructs, issuing compile-time errors if it encounters any syntax that requires transformation (rather than simple erasure).

This provides immediate feedback directly within your editor when attempting to use non-erasable features such as traditional enums, namespaces with runtime logic, parameter properties in classes, or import aliases. Rather than encountering runtime errors when executing your code with Node, TypeScript will alert you during development, significantly simplifying the process of writing code that is fully compatible with Node's type-stripping functionality.