Unlike languages like TypeScript, C#, or Java, JavaScript doesn't have native support for enums. However, there are several effective ways to implement enum-like behavior in JavaScript. This comprehensive guide explores 5 different methods to create enums in JavaScript, from simple object-based approaches to sophisticated class-based implementations. Whether you're working on a small project or a large-scale application, understanding these enum patterns will help you write more maintainable and type-safe JavaScript code.
What Are Enums in JavaScript?
An enum (enumeration) is a data type that consists of a set of named constants. In JavaScript, enums help you define a collection of related values that are typically used to represent a fixed set of options, states, or categories. They provide better code readability, prevent typos, and make your code more maintainable.
Common use cases for JavaScript enums include:
- Representing application states (loading, success, error)
- Defining user roles (admin, user, guest)
- Specifying configuration options (theme, language, mode)
- Managing API response statuses
- Defining game states or directions
Method 1: Object-Based Enums - The Simple Approach
The most straightforward way to create enums in JavaScript is using plain objects. This method is simple, widely supported, and perfect for basic use cases.
Basic Object Enum Implementation
// Simple object-based enum
const UserRole = {
ADMIN: 'admin',
USER: 'user',
GUEST: 'guest'
};
// Usage
function checkUserAccess(role) {
switch (role) {
case UserRole.ADMIN:
return 'Full access';
case UserRole.USER:
return 'Limited access';
case UserRole.GUEST:
return 'Read-only access';
default:
return 'No access';
}
}
console.log(checkUserAccess(UserRole.ADMIN)); // 'Full access'
console.log(checkUserAccess('admin')); // 'Full access'
Numeric Object Enums
// Numeric enum similar to TypeScript
const Priority = {
LOW: 0,
MEDIUM: 1,
HIGH: 2,
URGENT: 3
};
// Usage
function processTask(priority) {
if (priority >= Priority.HIGH) {
console.log('Processing high priority task');
} else if (priority >= Priority.MEDIUM) {
console.log('Processing medium priority task');
} else {
console.log('Processing low priority task');
}
}
processTask(Priority.URGENT); // 'Processing high priority task'
Getting All Enum Values
const Status = {
PENDING: 'pending',
APPROVED: 'approved',
REJECTED: 'rejected',
CANCELLED: 'cancelled'
};
// Get all enum values
const allStatuses = Object.values(Status);
console.log(allStatuses); // ['pending', 'approved', 'rejected', 'cancelled']
// Get all enum keys
const allKeys = Object.keys(Status);
console.log(allKeys); // ['PENDING', 'APPROVED', 'REJECTED', 'CANCELLED']
// Check if a value exists in the enum
function isValidStatus(status) {
return Object.values(Status).includes(status);
}
console.log(isValidStatus('pending')); // true
console.log(isValidStatus('invalid')); // false
Advantages:
- Simple and easy to understand
- Excellent browser support
- Lightweight with no dependencies
- Easy to iterate over values
Disadvantages:
- Values can be modified at runtime
- No type safety
- Can have duplicate values
- No protection against typos
Method 2: Frozen Object Enums - The Immutable Approach
Using Object.freeze()
makes your enum immutable, preventing accidental modifications while maintaining the simplicity of object-based
enums.
Basic Frozen Enum Implementation
// Frozen object enum
const Theme = Object.freeze({
LIGHT: 'light',
DARK: 'dark',
AUTO: 'auto'
});
// Attempting to modify will be silently ignored in strict mode or throw an error
// Theme.LIGHT = 'bright'; // This won't work
// Theme.NEW_THEME = 'new'; // This won't work
// Usage
function applyTheme(theme) {
switch (theme) {
case Theme.LIGHT:
document.body.classList.add('light-theme');
break;
case Theme.DARK:
document.body.classList.add('dark-theme');
break;
case Theme.AUTO:
// Apply system preference
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.body.classList.add(prefersDark ? 'dark-theme' : 'light-theme');
break;
}
}
applyTheme(Theme.DARK);
Advanced Frozen Enum with Helper Functions
// Enhanced frozen enum with utility functions
const HttpStatus = Object.freeze({
OK: 200,
CREATED: 201,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
INTERNAL_SERVER_ERROR: 500,
// Helper methods
isSuccess: function(code) {
return code >= 200 && code < 300;
},
isError: function(code) {
return code >= 400;
},
getMessage: function(code) {
const messages = {
200: 'OK',
201: 'Created',
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
500: 'Internal Server Error'
};
return messages[code] || 'Unknown Status';
}
});
// Usage
console.log(HttpStatus.isSuccess(200)); // true
console.log(HttpStatus.isError(404)); // true
console.log(HttpStatus.getMessage(404)); // 'Not Found'
Creating Frozen Enums with a Factory Function
// Factory function for creating frozen enums
function createEnum(values) {
const enumObject = {};
values.forEach(value => {
enumObject[value] = value;
});
// Add utility methods
enumObject.values = function() {
return Object.values(this);
};
enumObject.keys = function() {
return Object.keys(this);
};
enumObject.has = function(value) {
return Object.values(this).includes(value);
};
return Object.freeze(enumObject);
}
// Usage
const Direction = createEnum(['NORTH', 'SOUTH', 'EAST', 'WEST']);
console.log(Direction.NORTH); // 'NORTH'
console.log(Direction.values()); // ['NORTH', 'SOUTH', 'EAST', 'WEST']
console.log(Direction.has('NORTH')); // true
console.log(Direction.has('INVALID')); // false
Advantages:
- Immutable - values cannot be changed
- Simple to implement and understand
- Good performance
- Can include helper methods
Disadvantages:
- Still no type safety
- Can have duplicate values
- No protection against typos in property names
Method 3: Symbol-Based Enums - The Unique Approach
Using Symbols for enum values ensures that each enum value is unique and cannot be accidentally duplicated or confused with other values. This approach provides better type safety and prevents value collisions.
Basic Symbol Enum Implementation
// Symbol-based enum
const GameState = {
LOADING: Symbol('loading'),
MENU: Symbol('menu'),
PLAYING: Symbol('playing'),
PAUSED: Symbol('paused'),
GAME_OVER: Symbol('game_over')
};
// Usage
class Game {
constructor() {
this.state = GameState.LOADING;
}
setState(newState) {
this.state = newState;
}
isPlaying() {
return this.state === GameState.PLAYING;
}
canPause() {
return this.state === GameState.PLAYING;
}
}
const game = new Game();
game.setState(GameState.PLAYING);
console.log(game.isPlaying()); // true
console.log(game.canPause()); // true
Symbol Enums with Descriptions
// Symbol enum with additional metadata
const LogLevel = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn'),
ERROR: Symbol('error'),
FATAL: Symbol('fatal')
};
// Create a mapping for descriptions and colors
const LogLevelInfo = {
[LogLevel.DEBUG]: { description: 'Debug information', color: '#888' },
[LogLevel.INFO]: { description: 'General information', color: '#0066cc' },
[LogLevel.WARN]: { description: 'Warning message', color: '#ff9900' },
[LogLevel.ERROR]: { description: 'Error message', color: '#cc0000' },
[LogLevel.FATAL]: { description: 'Fatal error', color: '#990000' }
};
// Logger class using symbol enums
class Logger {
log(level, message) {
const info = LogLevelInfo[level];
console.log(
`%c[${info.description}] ${message}`,
`color: ${info.color}; font-weight: bold`
);
}
}
const logger = new Logger();
logger.log(LogLevel.INFO, 'Application started');
logger.log(LogLevel.ERROR, 'Something went wrong');
Symbol Enum Factory Function
// Factory function for symbol enums
function createSymbolEnum(values) {
const enumObject = {};
values.forEach(value => {
enumObject[value] = Symbol(value.toLowerCase());
});
// Add utility methods
enumObject.values = function() {
return Object.values(this);
};
enumObject.keys = function() {
return Object.keys(this);
};
enumObject.has = function(symbol) {
return Object.values(this).includes(symbol);
};
return Object.freeze(enumObject);
}
// Usage
const FileType = createSymbolEnum(['IMAGE', 'VIDEO', 'AUDIO', 'DOCUMENT']);
console.log(FileType.IMAGE); // Symbol(image)
console.log(FileType.IMAGE === FileType.VIDEO); // false
console.log(FileType.has(FileType.IMAGE)); // true
Advantages:
- Each value is guaranteed to be unique
- No value collisions between different enums
- Better type safety
- Cannot be accidentally duplicated
Disadvantages:
- Cannot be serialized to JSON
- More complex to debug
- Symbols are not enumerable by default
- Requires ES6+ support
Method 4: Class-Based Enums - The Advanced Approach
Class-based enums provide the most sophisticated approach, offering type safety, instance methods, and better semantic representation. This method is inspired by how TypeScript compiles enums and provides the most robust solution.
Basic Class-Based Enum Implementation
// Class-based enum
class UserRole {
constructor(name, permissions = []) {
this.name = name;
this.permissions = permissions;
}
hasPermission(permission) {
return this.permissions.includes(permission);
}
toString() {
return `UserRole.${this.name}`;
}
// Static enum values
static ADMIN = new UserRole('ADMIN', ['read', 'write', 'delete', 'admin']);
static USER = new UserRole('USER', ['read', 'write']);
static GUEST = new UserRole('GUEST', ['read']);
}
// Usage
function checkAccess(userRole, action) {
if (userRole.hasPermission(action)) {
console.log(`Access granted for ${userRole.name}`);
} else {
console.log(`Access denied for ${userRole.name}`);
}
}
checkAccess(UserRole.ADMIN, 'delete'); // 'Access granted for ADMIN'
checkAccess(UserRole.GUEST, 'write'); // 'Access denied for GUEST'
// Type checking
console.log(UserRole.ADMIN instanceof UserRole); // true
console.log(UserRole.ADMIN.toString()); // 'UserRole.ADMIN'
Advanced Class-Based Enum with Validation
// Advanced class-based enum with validation
class OrderStatus {
constructor(name, canTransitionTo = []) {
this.name = name;
this.canTransitionTo = canTransitionTo;
}
canTransitionToStatus(status) {
return this.canTransitionTo.includes(status);
}
transitionTo(newStatus) {
if (this.canTransitionToStatus(newStatus)) {
console.log(`Transitioning from ${this.name} to ${newStatus.name}`);
return newStatus;
} else {
throw new Error(`Cannot transition from ${this.name} to ${newStatus.name}`);
}
}
toString() {
return `OrderStatus.${this.name}`;
}
// Static enum values with transition rules
static PENDING = new OrderStatus('PENDING', ['CONFIRMED', 'CANCELLED']);
static CONFIRMED = new OrderStatus('CONFIRMED', ['SHIPPED', 'CANCELLED']);
static SHIPPED = new OrderStatus('SHIPPED', ['DELIVERED', 'RETURNED']);
static DELIVERED = new OrderStatus('DELIVERED', ['RETURNED']);
static CANCELLED = new OrderStatus('CANCELLED', []);
static RETURNED = new OrderStatus('RETURNED', []);
// Static utility methods
static getAllStatuses() {
return [
this.PENDING, this.CONFIRMED, this.SHIPPED,
this.DELIVERED, this.CANCELLED, this.RETURNED
];
}
static fromString(statusName) {
const status = this.getAllStatuses().find(s => s.name === statusName);
if (!status) {
throw new Error(`Invalid status: ${statusName}`);
}
return status;
}
}
// Usage
class Order {
constructor() {
this.status = OrderStatus.PENDING;
}
updateStatus(newStatus) {
try {
this.status = this.status.transitionTo(newStatus);
} catch (error) {
console.error(error.message);
}
}
}
const order = new Order();
order.updateStatus(OrderStatus.CONFIRMED); // Valid transition
order.updateStatus(OrderStatus.DELIVERED); // Invalid transition - throws error
Generic Enum Base Class
// Generic base class for creating enums
class Enum {
constructor(name, value) {
this.name = name;
this.value = value;
}
toString() {
return `${this.constructor.name}.${this.name}`;
}
valueOf() {
return this.value;
}
equals(other) {
return this === other;
}
// Static method to get all enum values
static values() {
return Object.values(this).filter(item => item instanceof this);
}
// Static method to get all enum names
static names() {
return Object.keys(this).filter(key => this[key] instanceof this);
}
// Static method to check if a value exists
static has(value) {
return this.values().includes(value);
}
}
// Usage with generic base class
class Priority extends Enum {
static LOW = new Priority('LOW', 1);
static MEDIUM = new Priority('MEDIUM', 2);
static HIGH = new Priority('HIGH', 3);
static URGENT = new Priority('URGENT', 4);
}
console.log(Priority.values()); // [Priority.LOW, Priority.MEDIUM, Priority.HIGH, Priority.URGENT]
console.log(Priority.has(Priority.HIGH)); // true
console.log(Priority.HIGH.toString()); // 'Priority.HIGH'
console.log(Priority.HIGH.valueOf()); // 3
Advantages:
- True type safety with instanceof checks
- Can include methods and properties
- Better semantic representation
- Supports complex logic and validation
- Typos in property names throw errors
Disadvantages:
- More complex to implement
- Larger memory footprint
- Cannot be serialized to JSON easily
- Requires ES6+ support
Method 5: TypeScript-Style Enums - The Compile-Time Approach
If you're using TypeScript or want to understand how TypeScript enums work, this section shows how to implement similar functionality in plain JavaScript. This approach provides the closest experience to native enum support.
Numeric Enum Implementation
// TypeScript-style numeric enum
const Direction = (function() {
const enumObject = {};
// Define enum values
enumObject['UP'] = 0;
enumObject[0] = 'UP';
enumObject['DOWN'] = 1;
enumObject[1] = 'DOWN';
enumObject['LEFT'] = 2;
enumObject[2] = 'LEFT';
enumObject['RIGHT'] = 3;
enumObject[3] = 'RIGHT';
// Add utility methods
enumObject.getKey = function(value) {
return this[value];
};
enumObject.getValue = function(key) {
return this[key];
};
enumObject.keys = function() {
return Object.keys(this).filter(key => isNaN(key));
};
enumObject.values = function() {
return Object.values(this).filter(value => typeof value === 'number');
};
return Object.freeze(enumObject);
})();
// Usage
console.log(Direction.UP); // 0
console.log(Direction[0]); // 'UP'
console.log(Direction.getKey(1)); // 'DOWN'
console.log(Direction.getValue('LEFT')); // 2
console.log(Direction.keys()); // ['UP', 'DOWN', 'LEFT', 'RIGHT']
console.log(Direction.values()); // [0, 1, 2, 3]
String Enum Implementation
// TypeScript-style string enum
const HttpMethod = (function() {
const enumObject = {};
enumObject['GET'] = 'GET';
enumObject['POST'] = 'POST';
enumObject['PUT'] = 'PUT';
enumObject['DELETE'] = 'DELETE';
enumObject['PATCH'] = 'PATCH';
// Add utility methods
enumObject.isValid = function(method) {
return Object.values(this).includes(method);
};
enumObject.isReadOnly = function(method) {
return method === this.GET;
};
enumObject.isWriteOperation = function(method) {
return [this.POST, this.PUT, this.DELETE, this.PATCH].includes(method);
};
return Object.freeze(enumObject);
})();
// Usage
function makeRequest(method, url) {
if (!HttpMethod.isValid(method)) {
throw new Error(`Invalid HTTP method: ${method}`);
}
console.log(`Making ${method} request to ${url}`);
if (HttpMethod.isWriteOperation(method)) {
console.log('This is a write operation');
}
}
makeRequest(HttpMethod.GET, '/api/users');
makeRequest(HttpMethod.POST, '/api/users');
Enum Factory for TypeScript-Style Enums
// Factory function for TypeScript-style enums
function createTypeScriptEnum(enumObject) {
const result = {};
// Copy all properties
Object.assign(result, enumObject);
// Add utility methods
result.keys = function() {
return Object.keys(this).filter(key => typeof this[key] !== 'function');
};
result.values = function() {
return Object.values(this).filter(value => typeof value !== 'function');
};
result.has = function(value) {
return Object.values(this).includes(value);
};
result.getKey = function(value) {
for (const [key, val] of Object.entries(this)) {
if (val === value && typeof val !== 'function') {
return key;
}
}
return undefined;
};
return Object.freeze(result);
}
// Usage
const Color = createTypeScriptEnum({
RED: 'red',
GREEN: 'green',
BLUE: 'blue',
YELLOW: 'yellow'
});
console.log(Color.RED); // 'red'
console.log(Color.getKey('blue')); // 'BLUE'
console.log(Color.has('green')); // true
Advantages:
- Bidirectional mapping (key to value and value to key)
- Similar to TypeScript enum behavior
- Good performance
- Familiar syntax for TypeScript developers
Disadvantages:
- More complex implementation
- Can be confusing with numeric enums
- Still no compile-time type checking
Real-World Examples and Use Cases
Example 1: API Response Status Management
// API response status enum
const ApiStatus = Object.freeze({
IDLE: 'idle',
LOADING: 'loading',
SUCCESS: 'success',
ERROR: 'error'
});
// React-like state management
class ApiState {
constructor() {
this.status = ApiStatus.IDLE;
this.data = null;
this.error = null;
}
setLoading() {
this.status = ApiStatus.LOADING;
this.error = null;
}
setSuccess(data) {
this.status = ApiStatus.SUCCESS;
this.data = data;
this.error = null;
}
setError(error) {
this.status = ApiStatus.ERROR;
this.error = error;
this.data = null;
}
isLoading() {
return this.status === ApiStatus.LOADING;
}
isSuccess() {
return this.status === ApiStatus.SUCCESS;
}
isError() {
return this.status === ApiStatus.ERROR;
}
}
// Usage
const userApi = new ApiState();
async function fetchUsers() {
userApi.setLoading();
try {
const response = await fetch('/api/users');
const data = await response.json();
userApi.setSuccess(data);
} catch (error) {
userApi.setError(error.message);
}
}
Example 2: Form Validation States
// Form field validation enum
const ValidationState = {
IDLE: Symbol('idle'),
VALIDATING: Symbol('validating'),
VALID: Symbol('valid'),
INVALID: Symbol('invalid')
};
// Form field class
class FormField {
constructor(name, validator) {
this.name = name;
this.validator = validator;
this.state = ValidationState.IDLE;
this.value = '';
this.error = null;
}
async validate(value) {
this.value = value;
this.state = ValidationState.VALIDATING;
try {
const isValid = await this.validator(value);
if (isValid) {
this.state = ValidationState.VALID;
this.error = null;
} else {
this.state = ValidationState.INVALID;
this.error = 'Invalid value';
}
} catch (error) {
this.state = ValidationState.INVALID;
this.error = error.message;
}
}
getStateClass() {
const stateClasses = {
[ValidationState.IDLE]: 'field-idle',
[ValidationState.VALIDATING]: 'field-validating',
[ValidationState.VALID]: 'field-valid',
[ValidationState.INVALID]: 'field-invalid'
};
return stateClasses[this.state];
}
}
// Usage
const emailField = new FormField('email', async (value) => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
});
await emailField.validate('[email protected]');
console.log(emailField.getStateClass()); // 'field-valid'
Example 3: Game State Management
// Game state enum with class-based approach
class GameState {
constructor(name, allowedTransitions = []) {
this.name = name;
this.allowedTransitions = allowedTransitions;
}
canTransitionTo(state) {
return this.allowedTransitions.includes(state);
}
toString() {
return `GameState.${this.name}`;
}
// Static enum values
static MENU = new GameState('MENU', ['PLAYING', 'SETTINGS']);
static PLAYING = new GameState('PLAYING', ['PAUSED', 'GAME_OVER', 'MENU']);
static PAUSED = new GameState('PAUSED', ['PLAYING', 'MENU']);
static GAME_OVER = new GameState('GAME_OVER', ['MENU', 'PLAYING']);
static SETTINGS = new GameState('SETTINGS', ['MENU']);
}
// Game class
class Game {
constructor() {
this.state = GameState.MENU;
this.score = 0;
this.level = 1;
}
changeState(newState) {
if (this.state.canTransitionTo(newState)) {
console.log(`State changed from ${this.state.name} to ${newState.name}`);
this.state = newState;
this.onStateChange();
} else {
console.error(`Cannot transition from ${this.state.name} to ${newState.name}`);
}
}
onStateChange() {
switch (this.state) {
case GameState.PLAYING:
this.startGame();
break;
case GameState.PAUSED:
this.pauseGame();
break;
case GameState.GAME_OVER:
this.endGame();
break;
}
}
startGame() {
console.log('Game started!');
}
pauseGame() {
console.log('Game paused');
}
endGame() {
console.log(`Game over! Final score: ${this.score}`);
}
}
// Usage
const game = new Game();
game.changeState(GameState.PLAYING); // Valid transition
game.changeState(GameState.SETTINGS); // Invalid transition - error
Performance Comparison
Understanding the performance characteristics of different enum implementations is important for choosing the right approach for your application.
Performance Test Results
// Performance test for different enum implementations
function performanceTest(name, setup, test, iterations = 1000000) {
const start = performance.now();
// Setup
const enumInstance = setup();
// Test
for (let i = 0; i < iterations; i++) {
test(enumInstance);
}
const end = performance.now();
console.log(`${name}: ${(end - start).toFixed(2)}ms`);
}
// Test setups
const objectEnum = () => ({ A: 'a', B: 'b', C: 'c' });
const frozenEnum = () => Object.freeze({ A: 'a', B: 'b', C: 'c' });
const symbolEnum = () => ({ A: Symbol('a'), B: Symbol('b'), C: Symbol('c') });
// Test functions
const objectTest = (enumObj) => enumObj.A === 'a';
const frozenTest = (enumObj) => enumObj.A === 'a';
const symbolTest = (enumObj) => enumObj.A === enumObj.A;
// Run tests
performanceTest('Object Enum', objectEnum, objectTest);
performanceTest('Frozen Enum', frozenEnum, frozenTest);
performanceTest('Symbol Enum', symbolEnum, symbolTest);
Memory Usage Comparison
// Memory usage comparison
function measureMemoryUsage(name, createEnum) {
const before = performance.memory ? performance.memory.usedJSHeapSize : 0;
const enums = [];
for (let i = 0; i < 1000; i++) {
enums.push(createEnum());
}
const after = performance.memory ? performance.memory.usedJSHeapSize : 0;
const memoryUsed = after - before;
console.log(`${name} memory usage: ${(memoryUsed / 1024).toFixed(2)} KB`);
}
// Test different enum types
measureMemoryUsage('Object Enum', () => ({ A: 'a', B: 'b', C: 'c' }));
measureMemoryUsage('Frozen Enum', () => Object.freeze({ A: 'a', B: 'b', C: 'c' }));
measureMemoryUsage('Symbol Enum', () => ({ A: Symbol('a'), B: Symbol('b'), C: Symbol('c') }));
Best Practices and Recommendations
When to Use Each Method
- Object-based enums: Simple projects, quick prototypes, when you need JSON serialization
- Frozen object enums: Production code where immutability is important
- Symbol-based enums: When you need guaranteed uniqueness and type safety
- Class-based enums: Complex applications requiring methods and validation
- TypeScript-style enums: When migrating from TypeScript or need bidirectional mapping
Code Quality Guidelines
- Use descriptive names: Choose clear, self-documenting enum names
- Group related enums: Organize enums in logical modules or namespaces
- Add validation: Include helper methods to validate enum values
- Document your enums: Add JSDoc comments explaining the purpose and usage
- Consider immutability: Use Object.freeze() to prevent accidental modifications
- Test your enums: Write unit tests for enum functionality
Common Pitfalls to Avoid
- Don't use magic strings: Always use enum values instead of hardcoded strings
- Avoid mutable enums: Use Object.freeze() to prevent runtime modifications
- Don't mix enum types: Be consistent with your enum implementation across the project
- Handle invalid values: Always validate enum values before using them
- Consider serialization: Choose the right enum type if you need JSON serialization
Integration with Modern JavaScript Features
Using Enums with ES6 Modules
// enums.js - Export enums from a module
export const UserRole = Object.freeze({
ADMIN: 'admin',
USER: 'user',
GUEST: 'guest'
});
export const ApiStatus = Object.freeze({
IDLE: 'idle',
LOADING: 'loading',
SUCCESS: 'success',
ERROR: 'error'
});
// userService.js - Import and use enums
import { UserRole, ApiStatus } from './enums.js';
class UserService {
async getUserRole(userId) {
// Implementation
return UserRole.USER;
}
async fetchUser(userId) {
// Implementation
return { status: ApiStatus.SUCCESS, data: {} };
}
}
Using Enums with TypeScript
// TypeScript enum
enum UserRole {
ADMIN = 'admin',
USER = 'user',
GUEST = 'guest'
}
// TypeScript with JavaScript enum
const Status = Object.freeze({
PENDING: 'pending',
APPROVED: 'approved',
REJECTED: 'rejected'
} as const);
type StatusType = typeof Status[keyof typeof Status];
function processStatus(status: StatusType) {
// TypeScript will provide autocomplete and type checking
switch (status) {
case Status.PENDING:
return 'Processing...';
case Status.APPROVED:
return 'Approved';
case Status.REJECTED:
return 'Rejected';
}
}
Using Enums with React
// React component using enums
import React, { useState } from 'react';
const LoadingState = Object.freeze({
IDLE: 'idle',
LOADING: 'loading',
SUCCESS: 'success',
ERROR: 'error'
});
function DataComponent() {
const [state, setState] = useState(LoadingState.IDLE);
const [data, setData] = useState(null);
const fetchData = async () => {
setState(LoadingState.LOADING);
try {
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
setState(LoadingState.SUCCESS);
} catch (error) {
setState(LoadingState.ERROR);
}
};
const renderContent = () => {
switch (state) {
case LoadingState.LOADING:
return Loading...;
case LoadingState.SUCCESS:
return Data: {JSON.stringify(data)};
case LoadingState.ERROR:
return Error loading data;
default:
return ;
}
};
return {renderContent()};
}
Browser Compatibility and Polyfills
Understanding browser support for different enum implementations is crucial for choosing the right approach for your project.
Browser Support Matrix
- Object-based enums: All browsers (IE6+)
- Object.freeze(): Modern browsers (IE9+)
- Symbols: Modern browsers (Chrome 38+, Firefox 36+, Safari 9+)
- Classes: Modern browsers (Chrome 49+, Firefox 45+, Safari 9+)
- ES6 Modules: Modern browsers with module support
Polyfills for Older Browsers
// Polyfill for Object.freeze
if (!Object.freeze) {
Object.freeze = function(obj) {
if (obj !== Object(obj)) {
throw new TypeError('Object.freeze can only be called on Objects.');
}
// Freeze the object
Object.getOwnPropertyNames(obj).forEach(function(name) {
var prop = obj[name];
if (typeof prop === 'object' && prop !== null) {
Object.freeze(prop);
}
});
return obj;
};
}
// Polyfill for Symbol (simplified)
if (!Symbol) {
window.Symbol = function(description) {
return 'Symbol(' + description + ')_' + Math.random().toString(36).substr(2, 9);
};
Symbol.for = function(key) {
if (!this._registry) {
this._registry = {};
}
if (!this._registry[key]) {
this._registry[key] = 'Symbol(' + key + ')_' + Math.random().toString(36).substr(2, 9);
}
return this._registry[key];
};
}
Conclusion
JavaScript enums provide a powerful way to define and work with fixed sets of values, improving code readability, maintainability, and reducing errors. While JavaScript doesn't have native enum support like TypeScript or other languages, the 5 methods we've explored offer different levels of functionality and type safety.
For most projects, frozen object enums provide the best balance of simplicity, performance, and immutability. When you need guaranteed uniqueness and better type safety, symbol-based enums are an excellent choice. For complex applications requiring methods and validation, class-based enums offer the most sophisticated solution.
The key to successful enum implementation is choosing the right approach for your specific needs and maintaining consistency throughout your codebase. By following the best practices outlined in this guide and considering the performance implications, you can create robust, maintainable JavaScript applications that leverage the power of enums effectively.
Remember that enums are just one tool in your JavaScript toolkit. Combine them with other modern JavaScript features like modules, classes, and TypeScript for even more powerful and type-safe applications. Whether you're building a simple web page or a complex enterprise application, understanding these enum patterns will help you write better, more maintainable JavaScript code.