TypeScript Enums vs Union Types vs as const Objects
enumsunion-typesas-consttypescripttype-safetycomparison

TypeScript Enums vs Union Types vs as const Objects

TTypeScript Page Editorial
2026-06-09
11 min read

A practical guide to choosing between TypeScript enums, union literals, and as const objects for safer finite-value modeling.

Choosing between TypeScript enums, union types, and as const objects is less about syntax preference and more about what you need your code to do at both type-check time and runtime. This guide compares the three common finite-value patterns, shows where each one fits, and gives practical defaults you can apply in frontend apps, Node services, shared libraries, and migration work. If you have ever wondered whether enums are still worth using, when a union is enough, or how as const creates a useful middle ground, this article is meant to give you a stable decision framework rather than a one-size-fits-all rule.

Overview

If you need to represent a fixed set of allowed values in TypeScript, you usually end up choosing one of these patterns:

  • Enum: a TypeScript construct that creates a named set of values and also emits runtime code in many cases.
  • Union of string or number literals: a type-only representation such as 'draft' | 'published' | 'archived'.
  • as const object: a JavaScript object frozen at the type level, often paired with a derived union type.

All three can model finite sets, but they optimize for different priorities.

Here is the shortest practical summary:

  • Use union types when you only need compile-time restrictions and want the simplest option.
  • Use as const objects when you need both runtime values and strong inferred types with minimal ceremony.
  • Use enums when you explicitly want enum semantics, are working in a codebase that already standardizes on them, or need them for interop with existing patterns.

That summary is useful, but it is incomplete. Real projects care about emitted JavaScript, tree-shaking, debugging clarity, API boundaries, object iteration, and how easy it is for a team to understand the code six months later. Those are the comparisons that matter.

For adjacent decisions about shaping types cleanly, see Interface vs Type in TypeScript: Current Best Practices.

How to compare options

The easiest way to compare enums, unions, and as const objects is to separate type-level needs from runtime needs. Many confusing choices come from mixing those two concerns.

1. Ask whether you need a runtime value

A union type disappears after compilation. That is often a strength, not a weakness. If all you need is to restrict accepted values in function parameters, state objects, or API result shapes, a union is usually enough.

But if you need to:

  • render a dropdown from a defined value set,
  • iterate over allowed values,
  • look up labels or metadata,
  • share a value map across runtime code,
  • or pass around a namespaced object of constants,

then a plain union type will not help by itself. You will need either an enum or an as const object.

2. Consider emitted JavaScript

Enums are not just types. In many cases they compile into runtime objects. That means they can affect bundle output and debugging. In contrast, union types emit nothing. as const objects emit exactly the JavaScript object you wrote, which makes them easier to reason about if you prefer plain JavaScript behavior.

For teams trying to keep build output unsurprising, this distinction matters more than style debates.

3. Think about developer ergonomics

Different patterns read differently in code reviews:

  • Enums give named access like Status.Published.
  • Unions are compact and direct, especially for local domain concepts.
  • as const objects often offer the best of both: namespaced values and a derived union type.

A good choice is one your team can read without needing to remember special compiler behavior.

4. Check interoperability needs

If your values cross boundaries between frontend and backend, validation becomes more important. A union type may model the allowed values cleanly, but runtime inputs are still unknown until you validate them. In those cases, a runtime-backed representation like an as const object can work well alongside schema validation libraries. If this is a recurring challenge in your codebase, How to Type API Responses in TypeScript Without Lying to the Compiler and Zod vs io-ts vs Valibot vs Yup for TypeScript Runtime Validation are useful companion reads.

5. Prefer the least powerful tool that still solves the problem

This is a stable rule. If a string union solves the problem, you do not gain much by introducing an enum. If you need runtime iteration and labels, a union alone is incomplete. If you need a constant object and a type derived from it, as const is often a clean middle path.

Feature-by-feature breakdown

This section compares the three patterns directly with concrete examples.

Enums

A basic string enum looks like this:

enum Status {
  Draft = 'draft',
  Published = 'published',
  Archived = 'archived'
}

function setStatus(status: Status) {
  // ...
}

setStatus(Status.Published)

What enums do well:

  • Provide a single named construct for related values.
  • Work well when a codebase already uses enum-style APIs.
  • Can feel familiar to developers coming from other languages.
  • Offer convenient namespacing with member access.

Where enums can be awkward:

  • They are not plain JavaScript syntax.
  • They often emit runtime code, which may be unnecessary for type-only use cases.
  • Numeric enums have behavior that can be surprising if you do not need reverse mappings.
  • Some teams avoid them to keep TypeScript closer to standard JavaScript patterns.

Editorial guidance: string enums are usually easier to reason about than numeric enums because the runtime values are explicit and readable. If you choose enums, prefer string enums unless you have a specific reason not to.

Union types

A union of literals is the minimal type-only solution:

type Status = 'draft' | 'published' | 'archived'

function setStatus(status: Status) {
  // ...
}

setStatus('published')

What unions do well:

  • Very small and expressive.
  • Emit no JavaScript.
  • Work naturally with narrowing, discriminated unions, and conditional logic.
  • Fit well in domain modeling, especially for props, state, and API shapes.

Where unions can be awkward:

  • There is no runtime object to iterate over.
  • You may repeat literal values in multiple places if you also need labels or metadata.
  • Calling code uses raw strings unless you wrap them in constants.

Editorial guidance: unions are often the best default for purely type-level constraints. They are especially strong in React props, reducer state, discriminated unions, and function parameters.

If you are building React applications, this style usually composes well with prop typing and narrowing patterns. For broader app structure, see Next.js + TypeScript Guide: App Router Patterns That Stay Type-Safe.

as const objects

This pattern creates a runtime object and a derived union:

const Status = {
  Draft: 'draft',
  Published: 'published',
  Archived: 'archived'
} as const

type Status = typeof Status[keyof typeof Status]

function setStatus(status: Status) {
  // ...
}

setStatus(Status.Published)

What as const objects do well:

  • Use normal JavaScript object syntax.
  • Provide runtime values for iteration and lookups.
  • Allow you to derive the union type from one source of truth.
  • Keep emitted code predictable because it is just the object you wrote.
  • Scale well when values need labels, descriptions, icons, or other metadata.

Where they can be awkward:

  • The derived type syntax is more verbose than a plain union.
  • Beginners may find typeof and keyof less approachable than enum syntax.
  • If overused for very small local cases, they can add noise.

Editorial guidance: this is often the most flexible enum alternative in modern TypeScript. It is a strong default when you need a runtime representation and type safety without enum-specific behavior.

Readability and autocomplete

All three patterns support editor autocomplete well, but they guide usage differently.

  • Enums encourage member access such as Status.Published.
  • Unions encourage direct literal usage such as 'published'.
  • as const objects support member access while preserving literal value types.

If your team prefers avoiding raw strings in calling code, unions may feel too loose even though they are type-safe. In that case, as const objects can offer a cleaner calling style.

Refactoring safety

All three are reasonably safe when used well, but the tradeoff is different:

  • With unions, renaming a value means updating literal strings wherever they appear.
  • With enums and as const objects, member access can make value usage easier to track.

That said, if your values are intentionally part of an external contract, raw literal strings can be clearer because they match the serialized form exactly.

Runtime validation and external data

None of these patterns validate external input on their own. A function parameter typed as Status is safe only after the value has already been checked or trusted. This matters in Node APIs, Express handlers, and browser forms.

An as const object can be especially helpful here because it gives you both a type and a runtime list of allowed values:

const Status = {
  Draft: 'draft',
  Published: 'published',
  Archived: 'archived'
} as const

type Status = typeof Status[keyof typeof Status]

const statusValues = Object.values(Status)

function isStatus(value: string): value is Status {
  return statusValues.includes(value as Status)
}

This is a common reason teams move from plain unions to as const objects in API-heavy code.

For backend-oriented setup patterns, see Express + TypeScript Starter Guide for APIs.

Discriminated unions

When modeling variants of objects, plain unions are often the clearest fit:

type Result =
  | { kind: 'success'; data: string }
  | { kind: 'error'; message: string }

You could use an enum or an as const object for kind, but many codebases find direct literal unions more readable inside discriminated unions. This is one of the strongest areas for literal unions because the values are close to the data they describe.

Bundle and build considerations

As a stable rule, unions have no runtime cost because they are erased. as const objects produce ordinary objects. Enums may introduce emitted structures that are useful in some cases and unnecessary in others. If your team cares about keeping output straightforward, or if you want code that maps closely to JavaScript mental models, this is a reasonable point in favor of unions and as const objects.

Build output questions often appear alongside tooling issues. If your project structure or imports start behaving strangely, these guides can help: How to Fix TypeScript Module Resolution Errors, TypeScript Path Aliases Guide for Vite, Next.js, Node, Jest, and ts-node, and TypeScript Monorepo Setup Guide: pnpm, Project References, and Shared Types.

Best fit by scenario

If you want the practical version, start here.

Use a union type when...

  • You only need compile-time restrictions.
  • The values are local to a component, function, or domain model.
  • You are building discriminated unions.
  • You want the smallest, clearest type expression.

Example use cases: React props, reducer action kinds, request status values in component state, small domain flags, return type variants.

Use an as const object when...

  • You need runtime values and a type from the same source.
  • You want to iterate over allowed values.
  • You want member access like Status.Published without introducing enums.
  • You need labels or metadata attached to values.

Example use cases: dropdown options, API status constants, shared frontend-backend value maps, feature flag keys, event names, route segment maps.

Use an enum when...

  • Your codebase already uses enums consistently.
  • You need enum semantics for interop or team conventions.
  • You value the explicit named construct enough to accept its tradeoffs.

Example use cases: established enterprise codebases, libraries that already expose enums, teams with strong enum-based style guidelines, migration paths where changing existing APIs would create churn.

A reasonable default for most modern code

If you need a simple rule of thumb:

  • Start with a union type.
  • Upgrade to an as const object if you also need runtime access.
  • Reach for an enum when there is a concrete reason, not just habit.

This default keeps your code close to JavaScript, minimizes unnecessary abstractions, and still leaves room for clear runtime modeling.

Migration advice for existing JavaScript code

When moving from JavaScript to TypeScript, it is usually easier to start with as const objects than enums. They preserve familiar object syntax and let you derive types incrementally. A plain constants object can often become type-safe with one change:

export const Roles = {
  Admin: 'admin',
  User: 'user',
  Guest: 'guest'
} as const

export type Role = typeof Roles[keyof typeof Roles]

This pattern tends to feel natural during migration because it works in both runtime logic and type declarations.

If your editor or linting setup is fighting you during cleanup, ESLint + TypeScript Flat Config Guide and How to Fix "Cannot find name" and Other Missing Type Errors in TypeScript can save time.

When to revisit

This topic is worth revisiting whenever your constraints change, because the best pattern is tied to how values are used, not to a permanent language rule.

Re-evaluate your choice when:

  • A type-only value set becomes runtime data. If a union starts needing iteration, labels, or validation, move toward an as const object.
  • Your code crosses more boundaries. Shared types between frontend, backend, and validation layers often benefit from one runtime-backed source of truth.
  • Your team standard changes. Some teams gradually move away from enums for predictability; others standardize on them for consistency. Both choices can be reasonable if made deliberately.
  • You introduce schema validation. Runtime validation libraries can make object-backed constants more attractive.
  • You publish or consume libraries. Public APIs tend to amplify tradeoffs around emitted code, readability, and compatibility.

Here is a practical review checklist you can apply before locking in a pattern:

  1. Do we need this only for static type checking, or also at runtime?
  2. Will we need to iterate over the values?
  3. Do we want callers to use raw literals or named members?
  4. Are these values part of a serialized external contract?
  5. Would a future developer understand this choice quickly?
  6. Can we keep one source of truth instead of duplicating values?

If you are starting fresh today, the safest action plan is simple:

  1. Model finite values with a union first.
  2. If runtime access appears, promote the values into an as const object and derive the union.
  3. Use enums where existing conventions or specific semantics justify them.

That approach avoids overengineering, supports gradual change, and keeps the codebase easier to maintain as TypeScript practices evolve.

In other words, the question is not whether enums, union literals, or as const objects are universally best. The better question is: what shape gives this value set the right balance of type safety, runtime usefulness, and clarity for this codebase right now? If you answer that directly, the choice usually becomes obvious.

Related Topics

#enums#union-types#as-const#typescript#type-safety#comparison
T

TypeScript Page Editorial

Senior SEO Editor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.

2026-06-09T06:44:08.470Z