How to Create and Use enum in TypeScript: Complete Guide with Source Code Analysis

TypeScript's enum declaration creates both a compile-time type for static checking and a runtime object mapping names to values, with const enum providing zero-runtime-cost alternatives through compile-time inlining.

The enum in TypeScript is implemented in the microsoft/TypeScript repository through a sophisticated compiler pipeline that transforms high-level declarations into optimized JavaScript output. Understanding how the compiler handles these declarations in src/compiler/parser.ts and src/compiler/emitter.ts enables developers to choose between standard enums, const enum optimizations, or enum-like alternatives based on their specific performance and type-safety requirements.

How TypeScript Compiles enum Declarations

When the TypeScript compiler encounters an enum declaration, it generates two distinct artifacts that serve different purposes in your application.

The Parse Phase: Creating EnumDeclaration Nodes

In src/compiler/parser.ts, the parseEnumDeclaration function constructs a syntax node of kind EnumDeclaration that represents the enum in the abstract syntax tree. This parser handles the full range of enum syntax, from simple numeric sequences to complex string initializers, and stores the member definitions for later type-checking and emission phases.

The Emit Phase: Generating Runtime Objects

The transformation into JavaScript occurs in src/compiler/emitter.ts, specifically within the emitEnumDeclaration function. For standard enums, this emitter generates a JavaScript object that maps enum names to their values. Numeric enums receive special treatment: the compiler creates bidirectional mappings where you can look up the name from the value (reverse mapping) in addition to the standard name-to-value lookup.

Standard enum in TypeScript: Numeric and String Variants

TypeScript supports two primary flavors of standard enums that differ in their runtime behavior and mapping capabilities.

Numeric Enums with Reverse Mapping

Numeric enums automatically generate reverse mappings, allowing you to access the enum member name from its numeric value:

enum Direction {
  Up = 1,
  Down,
  Left,
  Right,
}

The compiler emits an object where Direction.Up equals 1 and Direction[1] equals "Up". This bidirectional mapping is implemented in the emitter logic and consumes additional memory at runtime to support reverse lookups.

String Enums Without Reverse Mapping

String enums provide named constants with string values but do not create reverse mappings:

enum Status {
  Loading = "LOADING",
  Error = "ERROR",
  Success = "SUCCESS",
}

In src/compiler/utilities.ts, the isEnumConst helper function determines enum characteristics, while string enums emit only forward mappings (Status.Loading"LOADING"), resulting in smaller runtime footprints than their numeric counterparts.

const enum in TypeScript: Compile-Time Only Constants

Adding the const modifier to an enum declaration triggers aggressive compile-time optimizations that eliminate runtime objects entirely.

When processing a const enum, the compiler inlines the enum values directly into the usage sites. In src/compiler/transformers/ts.ts, the transformation logic replaces enum member references with their literal values based on the PreserveConstEnums compiler flag. If this flag is disabled, no runtime code is emitted:

const enum Permission {
  Read = 1 << 0,
  Write = 1 << 1,
  Execute = 1 << 2,
}

// Compiles to: const access = 1 | 2;
const access = Permission.Read | Permission.Write;

This inlining provides zero-runtime-cost constants but prevents iterating over enum members or using them as object keys at runtime.

Enum-Like Alternatives Without Runtime Overhead

For scenarios requiring only type safety without emitted JavaScript, TypeScript offers several enum-like patterns that leverage the type system without relying on src/compiler/factory/nodeFactory.ts to generate EnumDeclaration nodes.

Union Types for Pure Type Safety

A union of string literal types provides exhaustive checking without any runtime representation:

type Size = "small" | "medium" | "large";

function configure(s: Size) {
  // Type-safe compile-time checking with zero bytes emitted
}

This pattern avoids the runtime object creation entirely, unlike standard enums processed by the createEnumDeclaration factory function.

as const Assertions for Frozen Objects

The as const assertion creates immutable objects that can be used for iteration while extracting types:

const Role = {
  Admin: "ADMIN",
  User: "USER",
} as const;

type Role = keyof typeof Role; // "Admin" | "User"

This approach emits a frozen JavaScript object (unlike const enum, which emits nothing) while providing the same type-level exhaustiveness checking found in src/services/utilities.ts through SymbolFlags.Enum handling.

Ambient declare const enum

For type-only declarations that reference existing JavaScript constants, ambient declarations provide enum-like typing without emission:

declare const enum Mode {
  Read,
  Write,
}

This declares the type information used by the type checker in src/compiler/utilities.ts without invoking the emitEnumDeclaration logic, resulting in absolutely no output JavaScript.

Choosing the Right enum Pattern in TypeScript

Selecting the appropriate enum construct depends on specific runtime requirements and performance constraints:

  • Standard numeric enum – Use when you need bidirectional mapping (value → name) and runtime iteration capabilities. Best for configuration flags and status codes where reverse lookups are required.
  • Standard string enum – Use when you need forward mapping only with readable string values at runtime. Ideal for action types in Redux or event names.
  • const enum – Use when you need numeric constants with zero runtime overhead and no object allocation. Perfect for bitflags and performance-critical calculations.
  • Union types – Use when you need type safety without any JavaScript emission. Best for library APIs where bundle size is critical.
  • as const objects – Use when you need both type safety and runtime iteration over keys/values without enum-specific quirks.

Summary

  • Standard enum declarations processed by parseEnumDeclaration in src/compiler/parser.ts create runtime objects with member mappings.
  • Numeric enums generate bidirectional mappings (name ↔ value), while string enums create only unidirectional mappings (name → value).
  • const enum declarations are inlined at compile time by transformers in src/compiler/transformers/ts.ts, producing zero runtime overhead when PreserveConstEnums is disabled.
  • Enum-like alternatives such as union types and as const assertions provide type safety without invoking the emitter logic in src/compiler/emitter.ts.
  • The choice between enum types depends on whether you need reverse mapping, runtime iteration, or zero bundle size impact.

Frequently Asked Questions

What is the difference between enum and const enum in TypeScript?

Standard enum declarations emit a JavaScript object that maps member names to values (and values back to names for numeric enums), consuming runtime memory and enabling reverse lookups. In contrast, const enum values are inlined directly into the compiled output by the transformer logic in src/compiler/transformers/ts.ts, resulting in literal values with no runtime object creation. This means const enum members cannot be iterated or accessed dynamically using bracket notation.

Why does my numeric enum have reverse mappings?

Numeric enums implement bidirectional mappings as a language feature to support common use cases like converting numeric error codes to human-readable names. During emission in src/compiler/emitter.ts, the emitEnumDeclaration function generates both the forward mapping (Enum.Member = value) and the reverse mapping (Enum[value] = "Member"). String enums do not receive this treatment because string values are not suitable for reverse object keys in the generated JavaScript.

When should I use union types instead of enum in TypeScript?

Use union types when you require compile-time type safety without any runtime JavaScript emission, particularly in library code where minimizing bundle size is critical. Unlike enums processed through src/compiler/factory/nodeFactory.ts, union types exist only in the type system and are erased during compilation. They also prevent issues with enum merging (declaration merging) and provide stricter checking against accidental cross-enum compatibility that can occur with standard numeric enums.

How does TypeScript handle enum declarations internally?

The TypeScript compiler processes enum declarations through a multi-stage pipeline: src/compiler/parser.ts creates EnumDeclaration syntax nodes via parseEnumDeclaration, the type checker in src/services/utilities.ts validates members using SymbolFlags.Enum, and src/compiler/emitter.ts generates the final JavaScript output through emitEnumDeclaration. For const enum members, the compiler also utilizes isEnumConst from src/compiler/utilities.ts to determine whether to emit an object or inline the literal values during the transformation phase.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s https://instagit.com/install.md

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client