DEV Community

Maxim Logunov
Maxim Logunov

Posted on

TypeScript Enums Showdown: `const enum` vs `enum` vs `object as const`

Introduction

When working with TypeScript, developers often need to represent a set of related constants. While traditional enums are the obvious choice, TypeScript offers multiple approaches, each with distinct advantages and trade-offs. In this article, we'll explore three primary methods: const enum, regular enum, and object with as const, helping you make informed decisions for your projects.

1. Regular Enums (enum)

What they are: Traditional enums that exist both at compile time and runtime.

export enum UserRole {
  Admin = "ADMIN",
  User = "USER",
  Guest = "GUEST"
}
Enter fullscreen mode Exit fullscreen mode

Compilation output:

export var UserRole;
(function (UserRole) {
    UserRole["Admin"] = "ADMIN";
    UserRole["User"] = "USER";
    UserRole["Guest"] = "GUEST";
})(UserRole || (UserRole = {}));
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Runtime existence: Full object available during execution
  • Reflection support: Can iterate, validate, and introspect values
  • Cross-module compatibility: Works seamlessly across file boundaries
  • Familiar syntax: Traditional enum behavior from other languages

Cons:

  • Larger bundle size: Generates additional JavaScript code
  • Poor tree-shaking: Hard to eliminate unused values
  • Performance overhead: Object creation and property access

Best for: Public APIs, runtime validation, and scenarios needing introspection.

2. Const Enums (const enum)

What they are: Compile-time only enums that get completely erased from JavaScript output.

export const enum HttpStatus {
  OK = 200,
  Created = 201,
  NotFound = 404,
  ServerError = 500
}
Enter fullscreen mode Exit fullscreen mode

Compilation result: Values are inlined directly where used:

// Instead of HttpStatus.OK, you get:
const status = 200;
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Zero runtime overhead: No generated JavaScript code
  • Excellent performance: Values inlined directly
  • Perfect tree-shaking: No unused code remains
  • Small bundle size: Minimal impact on final build

Cons:

  • No runtime access: Cannot iterate or introspect
  • Module boundary issues: Problems with isolated compilation
  • Debugging challenges: Original enum names may be lost

Best for: Internal constants, performance-critical code, and simple value mappings.

3. Object with as const

What they are: TypeScript's modern approach using plain objects with const assertions.

export const UserRole = {
  Admin: "ADMIN",
  User: "USER",
  Guest: "GUEST"
} as const;

export type UserRole = typeof UserRole[keyof typeof UserRole];
Enter fullscreen mode Exit fullscreen mode

Compilation output: (Identical to input)

export const UserRole = {
  Admin: "ADMIN",
  User: "USER",
  Guest: "GUEST"
};
Enter fullscreen mode Exit fullscreen mode

Pros:

  • No runtime cost: Plain object, no extra generated code
  • Full type safety: Complete TypeScript integration
  • Runtime access: Full object available for introspection
  • Modern pattern: Aligns with TypeScript's evolution
  • Flexible values: Can mix types (strings, numbers, etc.)

Cons:

  • Manual type extraction: Requires additional type definition
  • No built-in reverse mapping: Unlike numeric enums
  • Slightly more verbose: Two declarations needed

Best for: Modern codebases, mixed value types, and when needing both compile-time and runtime access.

Comparative Analysis

Bundle Size Impact

// enum: ~150 bytes generated
enum Colors { Red, Green, Blue }

// const enum: 0 bytes generated  
const enum Colors { Red, Green, Blue }

// object as const: ~50 bytes (just the object)
const Colors = { Red: 0, Green: 1, Blue: 2 } as const
Enter fullscreen mode Exit fullscreen mode

Runtime Performance

// enum: Object property access
const color = Colors.Red;

// const enum: Direct value inline (fastest)
const color = 0;

// object as const: Object property access
const color = Colors.Red;
Enter fullscreen mode Exit fullscreen mode

Type Safety

All three approaches provide excellent type safety, but object as const offers the most flexibility with mixed types:

// Only possible with object as const
const Status = {
  Active: "ACTIVE",
  Pending: 0,
  Disabled: "DISABLED"
} as const;
Enter fullscreen mode Exit fullscreen mode

Practical Examples

Scenario 1: API Response Handling

// Best: object as const (needs runtime validation)
export const ApiErrorCode = {
  NotFound: "NOT_FOUND",
  Unauthorized: "UNAUTHORIZED",
  ServerError: "SERVER_ERROR"
} as const;

export type ApiErrorCode = typeof ApiErrorCode[keyof typeof ApiErrorCode];

function handleError(code: ApiErrorCode) {
  if (Object.values(ApiErrorCode).includes(code)) {
    // Valid error code
  }
}
Enter fullscreen mode Exit fullscreen mode

Scenario 2: Internal Constants

// Best: const enum (compile-time only)
export const enum Direction {
  Up = 1,
  Down = 2,
  Left = 3,
  Right = 4
}

function move(direction: Direction) {
  // Values get inlined: direction === 1, etc.
}
Enter fullscreen mode Exit fullscreen mode

Scenario 3: Public Library API

// Best: regular enum (runtime compatibility)
export enum LogLevel {
  Error = 0,
  Warn = 1,
  Info = 2,
  Debug = 3
}

// Consumers can iterate or validate
console.log(Object.values(LogLevel));
Enter fullscreen mode Exit fullscreen mode

Migration Example

From traditional enum:

enum OldStyle {
  Value1 = "VALUE1",
  Value2 = "VALUE2"
}
Enter fullscreen mode Exit fullscreen mode

To modern object as const:

const NewStyle = {
  Value1: "VALUE1",
  Value2: "VALUE2"
} as const;

type NewStyle = typeof NewStyle[keyof typeof NewStyle];
Enter fullscreen mode Exit fullscreen mode

Decision Guide

Use Case Recommended Approach
Public library API enum
Performance-critical code const enum
Runtime validation object as const
Mixed value types object as const
Legacy codebase enum (consistency)
New project object as const
Cross-module usage enum or object as const

Best Practices

  1. Use object as const for most new development
  2. Reserve const enum for performance-sensitive constants
  3. Choose enum when publishing libraries for wider consumption
  4. Consider tooling: Some build tools handle const enum poorly
  5. Be consistent within your codebase

Conclusion

TypeScript offers multiple ways to handle constants, each serving different needs:

  • enum: The traditional choice for runtime access and compatibility
  • const enum: The performance optimizer for compile-time only values
  • object as const: The modern, flexible approach for most use cases

Understanding these options allows you to write more efficient, maintainable TypeScript code. For most modern applications, object as const provides the best balance of type safety, runtime access, and bundle size efficiency, while const enum remains valuable for performance-critical scenarios and enum maintains its place in public APIs and legacy codebases.

Choose based on your specific needs, and remember that consistency within a project is often more important than absolute optimization.

Top comments (0)