DEV Community

Cover image for TypeScript Enums vs. String Unions: What's the Deal?
Denny Christochowitz
Denny Christochowitz

Posted on

TypeScript Enums vs. String Unions: What's the Deal?

Enums in TypeScript can be a bit of a double-edged sword. They look neat, give you nice tooling, and let you group related constants. But they also come with quirks, runtime implications, and compatibility oddities. If you've ever scratched your head wondering whether to use an enum or just go with string unions, this guide is for you.

Let's break it all down.


The Classic Enum

Here's a classic numeric enum in TypeScript:

enum Priority {
  low = 0,
  medium = 1,
  high = 2,
  critical = 3,
}
Enter fullscreen mode Exit fullscreen mode

You can use it like this:

const p = Priority.high;
console.log(Priority[p]); // 'high'
Enter fullscreen mode Exit fullscreen mode

πŸ”„ Reverse Mapping

With numeric enums, TypeScript generates a reverse mapping, so you can go from 2 to 'high'.

But there's a catch:

const key = Priority[2]; // inferred as `string`, not 'low' | 'medium' | ...
Enter fullscreen mode Exit fullscreen mode

You lose type safety unless you cast it:

const key = Priority[2] as keyof typeof Priority;
Enter fullscreen mode Exit fullscreen mode

Or wrap it in a helper:

function getEnumKeyByValue<E extends Record<string, string | number>>(
  enumObj: E,
  value: E[keyof E]
): keyof E | undefined {
  return (Object.keys(enumObj) as (keyof E)[]).find(
    (k) => enumObj[k] === value
  );
}
Enter fullscreen mode Exit fullscreen mode

Why Some Devs Avoid Enums

Enums in TypeScript compile to real JavaScript objects. That means:

  • They increase bundle size.
  • Structurally identical enums are not compatible.
  • You have to remember all the quirks (like reverse mapping only working for numeric enums).

They're not bad β€” just something to use with eyes open.


The String Union Alternative

Instead of an enum, you can write:

type Priority = 'low' | 'medium' | 'high' | 'critical';
Enter fullscreen mode Exit fullscreen mode

No runtime cost. Super simple. But now you don’t have a runtime object to loop over or validate against.


The Best of Both Worlds: Enum-Like Pattern

Here’s the slick trick: define a const array and infer the type from it.

const priorityValues = ['low', 'medium', 'high', 'critical'] as const;
type Priority = typeof priorityValues[number];
Enter fullscreen mode Exit fullscreen mode

βœ… Zero runtime bloat
βœ… Type-safe union
βœ… You can iterate over priorityValues
βœ… You can validate at runtime

function isPriority(input: string): input is Priority {
  return priorityValues.includes(input as Priority);
}
Enter fullscreen mode Exit fullscreen mode

πŸ” What About Object.fromEntries with Strong Typing?

You can build a typed object from a list of keys using Object.fromEntries. Here's how:

const entries: [Priority, string][] = priorityValues.map(
  (key) => [key, key.toUpperCase()] // just an example transformation
);

const priorityLabels = Object.fromEntries(entries) as Record<Priority, string>;
Enter fullscreen mode Exit fullscreen mode

Now priorityLabels.low is strongly typed, and accessing an invalid key will be a type error.

Or you can make it reusable with a helper:

function fromEntriesTyped<K extends string, V>(
  entries: readonly (readonly [K, V])[]
): Record<K, V> {
  return Object.fromEntries(entries) as Record<K, V>;
}

const entries = priorityValues.map((key) => [key, key.toUpperCase()] as const);
const priorityLabels = fromEntriesTyped(entries);
Enter fullscreen mode Exit fullscreen mode

A Reusable Enum Helper

Want to standardize this? Here's a tiny utility:

export function createEnum<const T extends readonly string[]>(values: T) {
  return {
    values,
    includes: (val: unknown): val is T[number] => values.includes(val as T[number]),
    type: null as unknown as T[number],
  };
}
Enter fullscreen mode Exit fullscreen mode

Usage:

const priorityEnum = createEnum(['low', 'medium', 'high', 'critical'] as const);
type Priority = typeof priorityEnum.type;
const PriorityValues = priorityEnum.values;
const isPriority = priorityEnum.includes;
Enter fullscreen mode Exit fullscreen mode

Now you’ve got everything: type safety, runtime values, validation, and no surprises.


TL;DR: When to Use What

Situation Go With
Small list, no runtime use βœ… String union
You want runtime access/iteration βœ… Const array + type
Need reverse mapping βœ… Numeric enum
Need labels or metadata βœ… Custom object

Enums are fine β€” but if you're aiming for minimalism, clarity, and flexibility, that const array + type combo is a real MVP.

Happy typing! πŸ§‘β€πŸ’»

Top comments (0)