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"
}
Compilation output:
export var UserRole;
(function (UserRole) {
UserRole["Admin"] = "ADMIN";
UserRole["User"] = "USER";
UserRole["Guest"] = "GUEST";
})(UserRole || (UserRole = {}));
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
}
Compilation result: Values are inlined directly where used:
// Instead of HttpStatus.OK, you get:
const status = 200;
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];
Compilation output: (Identical to input)
export const UserRole = {
Admin: "ADMIN",
User: "USER",
Guest: "GUEST"
};
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
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;
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;
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
}
}
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.
}
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));
Migration Example
From traditional enum:
enum OldStyle {
Value1 = "VALUE1",
Value2 = "VALUE2"
}
To modern object as const:
const NewStyle = {
Value1: "VALUE1",
Value2: "VALUE2"
} as const;
type NewStyle = typeof NewStyle[keyof typeof NewStyle];
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
-
Use
object as const
for most new development -
Reserve
const enum
for performance-sensitive constants -
Choose
enum
when publishing libraries for wider consumption -
Consider tooling: Some build tools handle
const enum
poorly - 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)