← Recipes

阅读中文版本

Refactor a String Enum to a Literal Union Type

Replace a runtime enum with a union of string literals, narrow at the boundaries, and verify exhaustiveness with a never check so missing cases fail to compile.

~20 mintypescriptrefactortypes

Task

Convert a string enum to a union of literal types so the values are erased at runtime and missing cases are caught at compile time.

When to use this

A status enum is used in a switch that silently falls through when a new status is added. The team wants the compiler to flag every missing case, and they want the enum gone from the runtime bundle.

Prerequisites

  • TypeScript 5 in strict mode
  • A codebase where the enum is used in switch statements
  • A test that covers every enum value

Steps

1. Define the union of string literals that replaces the enum.

type Status = "pending" | "active" | "archived";

Expected output: A union type that compiles to nothing at runtime, unlike an enum that emits a reverse mapping object.

2. Narrow the union at every switch and add a never default that fails to compile if a case is missing.

function label(s: Status) {
  switch (s) {
    case "pending": return "Pending";
    case "active": return "Active";
    case "archived": return "Archived";
    default: const _: never = s; return _;
  }
}

Expected output: Adding a new value to Status makes the never assignment fail to compile, pointing at every switch that needs a case.

3. Replace every enum reference with the union and delete the enum declaration.

rg -l "Status\." --type ts | xargs sed -i "s/Status\.pending/"pending"/g"

Expected output: No code references the enum, and the bundle no longer contains the reverse mapping object.

Troubleshooting

Problem

A value comes from the network and is not narrowed

Solution

Parse it through a schema that asserts the union, so an unknown value is rejected at the boundary instead of flowing into a switch.

Problem

The never check is too clever for the team

Solution

Extract it into an assertUnreachable helper so every switch reads the same way; the pattern is learned once.

Limitations

  • A union type has no runtime presence, so code that relied on Object.values(Enum) needs a separate const array.
  • The exhaustiveness check only fires in TypeScript; a JavaScript consumer still needs a default case.

Erase the enum, keep the types

A string enum emits a reverse mapping object at runtime. A union of string literals compiles to nothing. Replace the enum with the union and the bundle shrinks.

type Status = 'pending' | 'active' | 'archived';

Make the compiler check exhaustiveness

Add a never default to every switch. When a new value is added to the union, every switch that misses the case fails to compile, pointing exactly where to fix.

function label(s: Status) {
  switch (s) {
    case 'pending': return 'Pending';
    case 'active': return 'Active';
    case 'archived': return 'Archived';
    default: const _: never = s; return _;
  }
}

Expected outcome

The enum is gone from the runtime bundle, every switch is exhaustive, and adding a new status fails to compile until every case is handled. Values from the network are parsed through a schema that asserts the union, so an unknown value is rejected at the boundary.