TypeScript Enums
Enums in TypeScript provide a way to define a set of named constants. They allow you to create a collection of related values that can be used as a type. Enums make your code more readable, maintainable, and less prone to errors by giving meaningful names to sets of numeric or string values.
Enums
©Net Ninja
Enum Basics
Enums create named constants representing a fixed set of values, providing both compile-time type safety and runtime values. They define a collection of related constants that can be used as types or values throughout your application.
Enums improve code readability by replacing magic numbers or strings with meaningful names.
enum Direction {
North,
East,
South,
West,
}
// Using the enum
let myDirection: Direction = Direction.North;
console.log(myDirection); // 0
console.log(Direction.East); // 1
console.log(Direction[0]); // "North"
In this example:
We define an enum called
Direction
with four valuesBy default, enum values are assigned incremental numeric values starting from 0
You can use the enum as a type annotation (
let myDirection: Direction
)You can access the enum's numeric value or name using different syntaxes
Numeric Enums
Numeric enums assign incremental integer values starting from zero by default, though you can specify custom starting values. TypeScript automatically assigns subsequent values by incrementing from the previous value.
These enums support reverse mapping, allowing you to access both the name from the value and the value from the name.
enum Status {
Pending, // 0
Processing, // 1
Completed, // 2
Failed, // 3
}
let orderStatus: Status = Status.Processing;
console.log(orderStatus); // 1
Custom Numeric Values
You can assign specific numeric values to enum members, which is particularly useful for representing HTTP status codes, error codes, or other meaningful numeric constants. When you assign a value to one member, subsequent members continue auto-incrementing from that point unless explicitly assigned different values.
enum HttpStatus {
OK = 200,
Created = 201,
BadRequest = 400,
Unauthorized = 401,
NotFound = 404,
ServerError = 500,
}
function handleResponse(status: HttpStatus) {
if (status === HttpStatus.OK) {
console.log("Request successful");
} else if (status >= HttpStatus.BadRequest) {
console.log("Request failed");
}
}
handleResponse(HttpStatus.OK); // "Request successful"
handleResponse(HttpStatus.NotFound); // "Request failed"
When you assign a value to an enum member, subsequent members will be auto-incremented from that value:
enum Priority {
Low = 5,
Medium, // 6
High, // 7
Critical, // 8
}
Constant members are evaluated at compile time, while computed members are evaluated at runtime.
String Enums
String enums assign string values to each member, providing better debugging experience and more meaningful runtime values. Unlike numeric enums, string enums require explicit values for each member and don't support reverse mapping. They're ideal for representing states, categories, or any scenario where the string value has semantic meaning.
enum Direction {
North = "NORTH",
East = "EAST",
South = "SOUTH",
West = "WEST",
}
let myDirection: Direction = Direction.North;
console.log(myDirection); // "NORTH"
String enums have better readability and debugging experience compared to numeric enums because the values are meaningful when inspected at runtime. However, they don't support reverse mapping (you can't access Direction["NORTH"]
).
Const Enums
Const enums are completely inlined during compilation, removing the enum object from the generated JavaScript and replacing enum references with their literal values. This provides performance benefits and smaller bundle sizes but eliminates runtime enum operations like iteration or reverse mapping.
const enum Direction {
North,
East,
South,
West,
}
let myDirection = Direction.North;
At compilation, const enums are completely removed and their values are inlined wherever they're used. In the JavaScript output, you'll see:
let myDirection = 0; // Direction.North
This results in more efficient code but prevents certain runtime operations like reverse mapping.
Enum Member Types
Individual enum members can serve as specific types, enabling precise type constraints where only certain enum values are acceptable. This creates more granular type safety than using the entire enum as a type, particularly useful for discriminated unions and state management patterns.
enum ShapeKind {
Circle,
Square,
Triangle,
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
let circle: Circle = {
kind: ShapeKind.Circle,
radius: 10,
};
// This would cause a type error:
// let invalidCircle: Circle = {
// kind: ShapeKind.Square, // Error: Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'
// radius: 10
// };
In this example, we're using specific enum members (ShapeKind.Circle
and ShapeKind.Square
) as types for the kind
property, ensuring type safety.
Union Enums and Enum Member Types
Enums work seamlessly with union types, allowing you to create types that accept only specific subsets of enum values. This enables creating restricted APIs that only accept certain enum members while maintaining type safety and preventing invalid value usage.
enum Status {
Active,
Inactive,
Pending,
}
// Use a subset of enum values as a type
type AvailableStatus = Status.Active | Status.Pending;
function processUser(userId: string, status: AvailableStatus) {
// Process only users with Active or Pending status
}
// Valid
processUser("123", Status.Active);
processUser("456", Status.Pending);
// Invalid - will not compile
// processUser("789", Status.Inactive);
Enums at Runtime
Unlike other TypeScript type constructs, enums exist as real JavaScript objects at runtime, enabling dynamic operations like iteration and reflection. This runtime presence allows for more flexible programming patterns but also adds to bundle size, making const enums preferable when runtime features aren't needed.
enum Direction {
North,
East,
South,
West,
}
// You can pass the enum as a parameter
function printEnum(enumObject: any) {
Object.keys(enumObject)
.filter((key) => !isNaN(Number(key)))
.forEach((key) => {
console.log(`${key}: ${enumObject[key]}`);
});
}
printEnum(Direction);
// 0: "North"
// 1: "East"
// 2: "South"
// 3: "West"
This runtime presence allows for more dynamic operations with enums, but also increases the size of your generated JavaScript.
Practical Examples
Example 1: User Roles
enum UserRole {
Admin = "ADMIN",
Editor = "EDITOR",
Viewer = "VIEWER",
Guest = "GUEST",
}
interface User {
id: string;
name: string;
role: UserRole;
}
function checkAccess(user: User, requiredRole: UserRole): boolean {
// Simple role hierarchy check
switch (user.role) {
case UserRole.Admin:
return true; // Admin has access to everything
case UserRole.Editor:
return requiredRole !== UserRole.Admin;
case UserRole.Viewer:
return requiredRole === UserRole.Viewer || requiredRole === UserRole.Guest;
case UserRole.Guest:
return requiredRole === UserRole.Guest;
default:
return false;
}
}
const user: User = {
id: "user123",
name: "Alice",
role: UserRole.Editor,
};
console.log(checkAccess(user, UserRole.Viewer)); // true
console.log(checkAccess(user, UserRole.Admin)); // false
Example 2: State Machine for Order Processing
enum OrderState {
Created,
Processing,
Shipped,
Delivered,
Canceled,
}
class Order {
private state: OrderState;
constructor(
public id: string,
public customerName: string
) {
this.state = OrderState.Created;
}
getState(): OrderState {
return this.state;
}
getStateAsString(): string {
return OrderState[this.state];
}
processOrder(): boolean {
if (this.state === OrderState.Created) {
this.state = OrderState.Processing;
console.log(`Order ${this.id} is now being processed`);
return true;
}
console.log(`Cannot process order ${this.id} in ${this.getStateAsString()} state`);
return false;
}
shipOrder(): boolean {
if (this.state === OrderState.Processing) {
this.state = OrderState.Shipped;
console.log(`Order ${this.id} has been shipped`);
return true;
}
console.log(`Cannot ship order ${this.id} in ${this.getStateAsString()} state`);
return false;
}
deliverOrder(): boolean {
if (this.state === OrderState.Shipped) {
this.state = OrderState.Delivered;
console.log(`Order ${this.id} has been delivered`);
return true;
}
console.log(`Cannot deliver order ${this.id} in ${this.getStateAsString()} state`);
return false;
}
cancelOrder(): boolean {
if (this.state !== OrderState.Delivered && this.state !== OrderState.Canceled) {
this.state = OrderState.Canceled;
console.log(`Order ${this.id} has been canceled`);
return true;
}
console.log(`Cannot cancel order ${this.id} in ${this.getStateAsString()} state`);
return false;
}
}
// Usage
const order = new Order("ORD123", "John Doe");
console.log(`New order state: ${order.getStateAsString()}`); // "Created"
order.processOrder(); // "Order ORD123 is now being processed"
order.shipOrder(); // "Order ORD123 has been shipped"
order.deliverOrder(); // "Order ORD123 has been delivered"
// Try to cancel a delivered order
order.cancelOrder(); // "Cannot cancel order ORD123 in Delivered state"
Best Practices for Using Enums
1. Use PascalCase for enum names and enum members
PascalCase follows TypeScript conventions for types and provides consistency with interfaces, classes, and type aliases. This naming pattern immediately identifies enum constructs and maintains visual coherence across your codebase. Consistent naming also aligns with community standards and popular style guides.
// Good
enum HttpStatus {
OK = 200,
NotFound = 404,
}
// Not recommended
enum httpStatus {
ok = 200,
notFound = 404,
}
2. Use string enums for better readability
String enums provide meaningful values during debugging and runtime inspection, making issues easier to diagnose. When examining logs, network requests, or runtime values, string enum members are self-documenting and immediately understandable. This improves developer experience and reduces debugging time compared to numeric enums that require lookup tables.
// Better - values are meaningful when debugging
enum Direction {
North = "NORTH",
East = "EAST",
South = "SOUTH",
West = "WEST",
}
// Less clear at runtime
enum Direction {
North, // 0
East, // 1
South, // 2
West, // 3
}
3. Use const enums for better performance
Const enums eliminate runtime overhead by inlining values during compilation, resulting in smaller bundle sizes and faster execution. When you don't need runtime enum operations like iteration or reverse mapping, const enums provide performance benefits without sacrificing type safety. This optimization is particularly valuable in performance-critical applications.
// More efficient - values are inlined
const enum Direction {
North,
East,
South,
West,
}
// Regular enum creates runtime object
enum Direction {
North,
East,
South,
West,
}
4. Consider alternatives to enums when appropriate
Union types of string literals often provide better type safety and smaller compiled output than traditional enums. For simple value sets, union types eliminate runtime objects entirely while maintaining excellent type checking. Const assertions with objects can provide namespace-like organization with superior type inference and tree-shaking capabilities.
// Union of string literals
type Direction = "North" | "East" | "South" | "West";
// For constant values that need a namespace
const HttpStatus = {
OK: 200,
Created: 201,
BadRequest: 400,
NotFound: 404,
} as const;
type HttpStatus = (typeof HttpStatus)[keyof typeof HttpStatus];
Additional Best Practices
Keep Enums Focused: Each enum should represent a single conceptual group of related values rather than mixing different categories of constants.
Document Complex Enums: Use JSDoc comments to explain enum purpose, valid use cases, and any business rules associated with specific values.
Avoid Computed Members: Stick to literal values for enum members to maintain predictability and enable better optimization by the TypeScript compiler.
Be Careful with Numeric Enums: When using numeric enums, ensure the numeric values are meaningful and won't cause confusion if exposed in APIs or debugging scenarios.
Consider Enum Stability: Once an enum is part of a public API, changing member values can break compatibility, so choose values carefully from the start.
Exercises
Exercise 1: Weekdays
Description
In this exercise, you'll create a simple enum to represent the days of the week and use it to build a function that determines whether a day is a weekday or weekend.
Instructions
Create an enum called
DaysOfWeek
with seven values:Monday
,Tuesday
,Wednesday
,Thursday
,Friday
,Saturday
, andSunday
.Write a function called
isWeekend
that:Takes a parameter of type DaysOfWeek
Returns a boolean value: true if the day is a weekend (Saturday or Sunday), and false otherwise
Test your function with different days of the week.
Bonus: Create a function that returns the name of the next day given a current day.
Expected Output:
isWeekend(DaysOfWeek.Monday) should return false
isWeekend(DaysOfWeek.Saturday) should return true
nextDay(DaysOfWeek.Friday) should return DaysOfWeek.Saturday
Exercise 2: API Response Status
Description
In this exercise, you'll create a string enum to represent different API response statuses and build a function that simulates API responses with different statuses.
Instructions
Create a string enum called
ApiStatus
with at least four values:Success
,Error
,Loading
, andTimeout
. Assign appropriate string values to each.Write a function called
simulateApiCall
that:Takes a parameter
endpoint
of type stringReturns a Promise that resolves with an
ApiStatus
valueSimulates an API call by waiting for 1-2 seconds (using setTimeout)
Randomly selects and returns one of the API statuses, with a higher probability for Success
Write another function called
handleApiResponse
that:Takes an
ApiStatus
parameterLogs different messages based on the status
Test your functions by making several simulated API calls and handling the responses.
Expected Output:
Calling API...
API returned status: SUCCESS
Data retrieved successfully
Calling API...
API returned status: ERROR
An error occurred while fetching data
Exercise 3: User Permissions System
Description
In this exercise, you'll create an enum-based permission system for a simple user management application. The system will define different permission levels and provide functions to check and manage user access.
Instructions
Create an enum called
UserPermission
with the following values:None
Read
Write
Delete
Admin
Create a
User
interface with:id
: stringname
: stringpermission
: UserPermission
Create an array of sample users with different permission levels.
Write the following functions:
canReadData(user: User): boolean
- returns true if user has Read permission or highercanWriteData(user: User): boolean
- returns true if user has Write permission or highercanDeleteData(user: User): boolean
- returns true if user has Delete permission or higherisAdmin(user: User): boolean
- returns true if user has Admin permissionpromoteUser(user: User): User
- returns a new user object with permission increased by one level (up to Admin)demoteUser(user: User): User
- returns a new user object with permission decreased by one level (down to None)
Test your functions with the sample users to verify they work correctly.
Expected Output:
User John (Read permission):
Can read: true
Can write: false
Can delete: false
Is admin: false
After promotion:
User John now has Write permission
Can read: true
Can write: true
Summary
TypeScript enums provide a powerful way to define sets of named constants that improve code readability and maintainability:
Numeric enums: The default, where members have numeric values
String enums: Where members have string values for better readability
Const enums: For better performance by inlining values at compile time
Computed members: Dynamic values determined at runtime
Key best practices:
Use PascalCase for enum names and members
Prefer string enums for better debugging
Use const enums for performance-critical code
Consider alternatives like union types when appropriate
Use powers of 2 for bit flags
Enums are a valuable tool in TypeScript's type system, helping you express intent more clearly and catch errors at compile time rather than runtime.