Enums vs. Union Literal Types in TypeScript: Compared
AI generated
type
TypeScript
Enums vs. Union Literal Types: What to Use When
Two ways to express a fixed set of allowed values, with different trade-offs

TypeScript offers two fundamentally different tools for representing a fixed set of allowed values: enums and union literal types. Both have legitimate use cases, but the choice has real consequences for bundle size, serialization, and interoperability.

9 min read Language design Best practices

1. Two Approaches to a Fixed Set of Values

Both enums and union literal types solve the same underlying problem: a variable should only ever hold one of several predefined values. How that goal is reached, however, differs fundamentally. Enums create an actual runtime construct, while union literal types exist purely within the type system.

This difference in nature has consequences that go well beyond taste: bundle size, JSON serialization behavior, debugging experience, and interoperability with plain JavaScript code are all affected.

Teams coming from languages with classic enums are often tempted to reach for TypeScript enums out of habit. A deliberate comparison of both approaches pays off before a convention becomes locked in across an entire project.

2. Numeric Enums and Their Quirks

A numeric enum creates a runtime object with a so-called reverse mapping property: not only does the name point to the value, the value also points back to the name. That's handy for debugging output, but it doubles the number of entries in the generated JavaScript object.

Another risk of numeric enums is their implicit compatibility with any number. TypeScript allows any number variable to be passed where a numeric enum is expected, without the compiler flagging it.


enum Status {
  Draft,
  Published,
  Archived,
}

console.log(Status.Published); // 1
console.log(Status[1]);        // "Published" (reverse mapping)

function setStatus(s: Status) {}
setStatus(42); // not flagged by the compiler

3. String Enums and const enum

String enums avoid the reverse mapping issue and the implicit compatibility with arbitrary numbers, since every member gets an explicit string value. They're considerably safer than numeric enums, but they still create a real runtime object.

A const enum, by contrast, is fully resolved at compile time and leaves no runtime object behind at all: every usage is replaced directly by its literal value. This saves on bundle size, but it doesn't work with the isolatedModules compiler option required by tools like esbuild, swc, or Babel, since those transpile files in isolation without being able to know an enum's value.


enum ExportFormat {
  Json = "json",
  Xml = "xml",
  Csv = "csv",
}

const enum LogLevel {
  Info = "info",
  Warn = "warn",
  Error = "error",
}

// const enum gets replaced by "info" at compile time
console.log(LogLevel.Info);

4. Union Literal Types as an Alternative

A union literal type defines the allowed set of values purely in the type system, with no runtime representation whatsoever. At runtime, the values are plain strings or numbers, which greatly simplifies interoperability with plain JavaScript code, JSON APIs, and libraries without their own enum support.

Since no extra object is created, there's no bundle-size overhead at all. For discriminated unions, a central pattern in type-safe TypeScript code, union literal types are the natural choice anyway.


type ExportFormat = "json" | "xml" | "csv";

function exportData(format: ExportFormat) {
  switch (format) {
    case "json":
      return toJson();
    case "xml":
      return toXml();
    case "csv":
      return toCsv();
  }
}

5. Structural vs. Nominal Behavior

An often overlooked difference concerns type checking itself. Enums behave in a partially nominal way in TypeScript: two different enums with identical values are not mutually assignable, even when the underlying values match. Union literal types, by contrast, are purely structural, a string literal is always compatible no matter which context it came from.

This nominal behavior can actually serve as a feature in large codebases, preventing accidental mixing of similar value sets, but it also makes interoperability with external data harder.

Anyone wanting the best of both worlds often reaches for branded types, a technique built on top of union literal types that adds an artificial distinguishing marker to prevent accidental mixing of similar string types, without taking on the downsides of real enums.

6. JSON Serialization and API Boundaries

At API boundaries, for example when receiving JSON data from a backend, real enum instances never actually arrive, only plain strings or numbers. With union literal types this matches the runtime representation exactly, so no conversion is needed.

With enums, the raw value received must instead be explicitly checked against the allowed enum values or converted into the enum type, which requires extra code at every system boundary.


type Status = "draft" | "published" | "archived";

async function fetchStatus(): Promise<Status> {
  const res = await fetch("/api/status");
  const data = await res.json();
  return data.status as Status; // directly compatible
}

7. When Enums Are Actually the Right Choice

Despite the drawbacks above, enums still have legitimate use cases. For bitflag patterns, where several values are combined via bitwise operations, numeric enums remain useful, since union literal types offer no native support for such combinations.

When true runtime iteration over all possible values is needed, for example to populate a dropdown menu dynamically, an enum object provides that directly, whereas a union literal type requires maintaining a separate constant listing all values.

8. Practical Recommendation

For most use cases in modern TypeScript code, especially discriminated unions, API response types, and configuration values, union literal types are the more pragmatic choice: no runtime overhead, straightforward serialization, and full compatibility with plain JavaScript.

When true runtime iteration over all values is required, a pattern combining an as const array with a union type derived from it offers the best of both worlds without needing a real enum.


const STATUSES = ["draft", "published", "archived"] as const;
type Status = (typeof STATUSES)[number];

// STATUSES.map(...) enables runtime iteration
// Status remains a pure union literal type

9. Migration Pitfalls

When replacing existing enums with union literal types, keep in mind that const enum values are often compared directly against numbers or strings in downstream code. A migration therefore requires a careful search for every usage site, especially for numeric enums, whose concrete values are often implicitly assumed.

Another pitfall: const enum doesn't work in projects using isolatedModules, which has even become the default for many standard configurations since TypeScript 5.0. Anyone wanting to keep using const enum has to explicitly disable that option, which can in turn limit other build tools.

Tests deserve a second look too: snapshot tests relying on the reverse mapping of numeric enums often break silently when the order of enum members changes. Union literal types naturally avoid this risk, since they carry no automatically generated numeric values.

Aspect Numeric enum String enum const enum Union literal type
Runtime object Yes, with reverse mapping Yes No, replaced inline No
Bundle overhead Yes Yes None None
Compatible with isolatedModules Yes Yes No Yes
JSON serialization Requires conversion Requires conversion Directly compatible Directly compatible
Type checking Partially nominal Partially nominal Partially nominal Purely structural

Mironsoft

TypeScript migration, type safety, and team onboarding

A JavaScript codebase without type safety, but no time for a full migration?

We migrate existing JavaScript projects to TypeScript step by step, set up strict compiler settings cleanly, and bring teams to the same type-safety level with code reviews and style guides.

Migration Roadmap

Plan and execute a gradual JS-to-TS migration without big-bang risk.

Strict Mode Rollout

Set up tsconfig.json, ESLint rules, and CI checks for lasting type safety.

Team Onboarding

Bring developers up to speed on TypeScript best practices with workshops and reviews.

10. Summary

Enums vs. Union Literals

Enum runtime overhead

Real object per enum

Union runtime overhead

None, pure type system

const enum limitation

Incompatible with isolatedModules

Recommendation

Union literals as the default choice

11. FAQ: Enums vs. Union Literals

1What is the fundamental difference between enums and union literal types?
Enums create a real runtime object in JavaScript, while union literal types exist purely in the type system and remain plain values like strings or numbers at runtime. That difference directly affects bundle size and interoperability.
2Why are numeric enums considered riskier than string enums?
Numeric enums let the compiler accept any number as a valid enum value, which can leave typos or wrong values undetected. String enums are not affected by this and are generally considered safer.
3What does const enum do differently from a normal enum?
const enum is fully resolved at compile time and produces no runtime object. Every usage is replaced directly by its literal value, which saves bundle size but adds constraints with certain build tools.
4Why doesn't const enum work with isolatedModules?
Tools that use isolatedModules transpile every file in isolation and therefore don't know the concrete value of a const enum defined in another file, making inline replacement impossible and triggering a compiler error.
5Why are union literal types often the better choice for JSON APIs?
Since union literal types are plain strings or numbers at runtime, this matches the format of JSON data exactly, so no conversion between raw data and enum instances is needed at any system boundary.
6What does nominal behavior mean for enums?
Two different enums with identical values are not mutually assignable in TypeScript, even though the underlying values match. Union literal types, by contrast, behave purely structurally and stay more flexibly compatible.
7When are enums still a good choice?
For bitflag patterns involving bitwise combinations of several values, and in cases requiring true runtime iteration over all values, enums provide direct support that union literal types don't offer out of the box.
8How can runtime iteration be achieved with union literal types?
Through an as const array, from which both the union type can be derived using typeof and index access, and a real runtime list can be used for iteration, without needing an actual enum.
9What migration risks exist when switching from enums to union literals?
Existing code that implicitly relies on concrete enum values, especially for numeric enums, needs careful review, since runtime behavior changes at every usage site when switching to union literal types.
10What's the general recommendation for new TypeScript projects?
For most use cases, especially discriminated unions and API types, union literal types are the more pragmatic choice. Enums remain relevant for bitflags and explicit runtime iteration and should be used deliberately.