JavaScript emit6 min read

TypeScript Enums Emit JavaScript Objects

See what numeric, string, and const enums emit, when reverse mappings appear, and when an as-const object is a safer API.

  • enums
  • JavaScript emit
  • API design

A regular TypeScript enum is both a type and a runtime value. The compiler normally emits a JavaScript object for it. Numeric enums add reverse mappings from values back to member names; string enums do not. A const enum takes a different route by inlining member values, unless preserveConstEnums is enabled.

Those emit choices matter when an enum crosses a package boundary, appears in serialized data, or must run in a tool that only strips types. Choose an enum because the runtime object is useful, not because it merely looks like a concise union.

Numeric enums create forward and reverse mappings

Numeric members auto-increment when an initializer is omitted. This example assigns 200 and 201:

enum StatusCode {
  Ok = 200,
  Created,
}

console.log(StatusCode.Ok);   // 200
console.log(StatusCode[200]); // "Ok"

The compiler emits assignments that populate the object in both directions. The TypeScript enum handbook documents the generated reverse mapping: a name maps to its number, and that number maps back to a name.

This also affects enumeration. Object.keys(StatusCode) contains both names and numeric-looking keys, so it is usually the wrong way to obtain only the declared member names.

Duplicate numeric values make reverse lookup lossy:

enum ExitCode {
  Success = 0,
  AlsoSuccess = 0,
}

console.log(ExitCode[0]); // "AlsoSuccess"

The later assignment wins in the runtime object. If stable names or wire values matter, do not infer them from a numeric reverse lookup.

String enums emit only name-to-value properties

String members must be initialized with string constants. Their emitted object has no reverse mapping:

enum LogLevel {
  Info = 'info',
  Error = 'error',
}

console.log(LogLevel.Error); // "error"
console.log(Object.keys(LogLevel)); // ["Info", "Error"]

The official enum documentation distinguishes string-enum emit from numeric reverse mappings. Readable string values are generally safer than auto-incremented numbers for logs, storage, and network messages, but an enum still does not validate an untrusted string. A parser must establish that an input is one of the allowed values.

At the type level, enum member names and values are different sets. Use keyof typeof LogLevel for the names:

type LogLevelName = keyof typeof LogLevel;
// "Info" | "Error"

function valueFor(name: LogLevelName): LogLevel {
  return LogLevel[name];
}

const enum removes the object by default

A const enum can use only constant enum expressions. TypeScript normally replaces each reference with its value and removes the declaration:

const enum Direction {
  Up,
  Down,
}

const next = Direction.Down;

The JavaScript output is effectively const next = 1, often with a comment naming the member. There is no Direction object to reflect over or pass to another function. The handbook’s const-enum section describes both the inlining and the restrictions.

Inlining is especially risky across published declaration files. A consumer can compile against one package version and run against another, leaving old numeric values embedded in its JavaScript. Ambient const enums also conflict with isolatedModules, because a single-file transform cannot reliably inline values declared elsewhere.

preserveConstEnums changes the producer’s JavaScript emit so the enum object remains, while references in the same project can still be inlined. The official preserveConstEnums reference shows that preserved const enums emit runtime objects like regular enums. Library authors must also remove const from generated declarations if downstream consumers should avoid inlining.

Type-stripping runtimes cannot execute enums

Enums require JavaScript generation, so a runtime that only erases type annotations cannot implement them. Node.js 26 built-in type stripping, for example, rejects enum declarations with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX. The TypeScript 5.8 erasableSyntaxOnly option can report these constructs during type-checking before the runtime sees them.

Use a compiler or transformer when enums are part of the source language. If direct type-stripped execution is a requirement, use JavaScript syntax that already has runtime meaning.

An as-const object separates the value from its union

A plain object can provide named runtime values without TypeScript-specific emit:

const LogLevels = {
  Info: 'info',
  Error: 'error',
} as const;

type LogLevelValue = typeof LogLevels[keyof typeof LogLevels];
// "info" | "error"

function write(level: LogLevelValue, message: string): string {
  return `[${level}] ${message}`;
}

console.log(write(LogLevels.Info, 'ready'));

This pattern emits the object literal that is already visible in the source. It works with type stripping, tree-shaking tools can reason about ordinary JavaScript properties, and callers can use the string-literal union without depending on an enum declaration. The tradeoff is the extra type alias and the absence of automatic numeric assignment or reverse mapping.

Use a regular enum when a named runtime object, enum member types, and TypeScript’s enum semantics are the intended contract. Use an as-const object when ordinary JavaScript values and a derived union are enough. Keep const enums private to one compilation unless every consumer and version boundary is under your control.