Enums (enumerations) allow you to define a set of named constants. They're perfect for representing a fixed set of related values like days of the week, status codes, or user roles. Enums make your code more readable, type-safe, and easier to maintain. Let's explore TypeScript enums in depth!
What Are Enums?
An enum is a special TypeScript feature that allows you to define a set of named constants. Instead of using magic strings or numbers throughout your code, you can use descriptive enum members.
Plain Constants vs Enums
// JavaScript - Using plain objects or strings
const STATUS_PENDING = "pending";
const STATUS_APPROVED = "approved";
const STATUS_REJECTED = "rejected";
// Easy to make typos
let orderStatus = "pendding"; // Typo!
// No autocomplete
if (orderStatus === "aprroved") { // Another typo!
console.log("Order approved");
}
// Magic strings everywhere
function updateStatus(status) {
// Is "active" a valid status? No idea!
if (status === "active") {
// ...
}
}// TypeScript - Using enums
enum OrderStatus {
Pending = "pending",
Approved = "approved",
Rejected = "rejected"
}
// Type-safe, autocomplete works
let orderStatus: OrderStatus = OrderStatus.Pending;
// TypeScript catches typos
if (orderStatus === OrderStatus.Approved) {
console.log("Order approved");
}
// Clear what values are valid
function updateStatus(status: OrderStatus) {
if (status === OrderStatus.Pending) {
// TypeScript knows all possible values
}
}Why TypeScript is Better: Enums provide a type-safe way to define named constants. They prevent typos, enable autocomplete, and make code more readable and maintainable.
Key TypeScript Benefits
- Type safety catches errors before runtime
- Better IDE autocomplete and IntelliSense
- Self-documenting code through types
- Easier refactoring and maintenance
- Fewer runtime errors in production
Why Use Enums?
- Type Safety: Prevent invalid values
- Autocomplete: IDEs suggest valid enum members
- Readability: Self-documenting code
- Refactoring: Easy to rename values everywhere
- No Magic Values: Clear what values are valid
Numeric Enums
Numeric enums are the default in TypeScript. Each member is assigned a numeric value, starting from 0 by default.
Basic Numeric Enums
// Basic numeric enum (starts at 0)
enum Direction {
North, // 0
South, // 1
East, // 2
West // 3
}
// Using the enum
let playerDirection: Direction = Direction.North;
console.log(Direction.North); // 0
console.log(Direction.South); // 1
console.log(Direction.East); // 2
console.log(Direction.West); // 3
// Type-safe comparisons
if (playerDirection === Direction.North) {
console.log("Moving north");
}
// Get enum name from value (reverse mapping)
console.log(Direction[0]); // "North"
console.log(Direction[2]); // "East"Custom Starting Values
// Start from 1 instead of 0
enum Month {
January = 1,
February, // 2
March, // 3
April, // 4
May, // 5
June, // 6
July, // 7
August, // 8
September, // 9
October, // 10
November, // 11
December // 12
}
console.log(Month.January); // 1
console.log(Month.December); // 12
let currentMonth: Month = Month.March;
console.log(`Month: ${currentMonth}`); // 3Fully Initialized Enums
// Explicitly set all values
enum HttpStatus {
OK = 200,
Created = 201,
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404,
InternalServerError = 500
}
// Using in a function
function handleResponse(status: HttpStatus): string {
switch (status) {
case HttpStatus.OK:
return "Success";
case HttpStatus.Created:
return "Resource created";
case HttpStatus.BadRequest:
return "Invalid request";
case HttpStatus.NotFound:
return "Resource not found";
case HttpStatus.InternalServerError:
return "Server error";
default:
return "Unknown status";
}
}
console.log(handleResponse(HttpStatus.OK)); // "Success"
console.log(handleResponse(HttpStatus.NotFound)); // "Resource not found"
// Actual values
console.log(HttpStatus.OK); // 200
console.log(HttpStatus.NotFound); // 404💡 Reverse Mapping
Numeric enums have reverse mapping—you can get the enum name from its value: Direction[0] returns "North". This doesn't work with string enums!
String Enums
String enums have string values instead of numbers. They're more readable in logs and debugging.
Basic String Enums
// String enum - all members must be initialized
enum OrderStatus {
Pending = "PENDING",
Processing = "PROCESSING",
Shipped = "SHIPPED",
Delivered = "DELIVERED",
Cancelled = "CANCELLED"
}
let orderStatus: OrderStatus = OrderStatus.Pending;
console.log(orderStatus); // "PENDING"
// Great for API responses
function getStatusMessage(status: OrderStatus): string {
switch (status) {
case OrderStatus.Pending:
return "Your order is pending";
case OrderStatus.Processing:
return "Your order is being processed";
case OrderStatus.Shipped:
return "Your order has been shipped";
case OrderStatus.Delivered:
return "Your order has been delivered";
case OrderStatus.Cancelled:
return "Your order was cancelled";
}
}
console.log(getStatusMessage(OrderStatus.Shipped));
// "Your order has been shipped"String Enums for User Roles
// User roles with string enum
enum UserRole {
Admin = "ADMIN",
Moderator = "MODERATOR",
User = "USER",
Guest = "GUEST"
}
// User type using the enum
type User = {
id: number;
name: string;
role: UserRole;
};
// Check permissions
function canDeletePost(user: User): boolean {
return user.role === UserRole.Admin ||
user.role === UserRole.Moderator;
}
function canEditSettings(user: User): boolean {
return user.role === UserRole.Admin;
}
// Create users
let admin: User = {
id: 1,
name: "Yusuf Ibrahim",
role: UserRole.Admin
};
let regularUser: User = {
id: 2,
name: "Amina Hassan",
role: UserRole.User
};
console.log(`Can ${admin.name} delete posts? ${canDeletePost(admin)}`);
// "Can Yusuf Ibrahim delete posts? true"
console.log(`Can ${regularUser.name} delete posts? ${canDeletePost(regularUser)}`);
// "Can Amina Hassan delete posts? false"String Enums for API Endpoints
// API endpoints as string enum
enum ApiEndpoint {
Users = "/api/users",
Products = "/api/products",
Orders = "/api/orders",
Auth = "/api/auth",
Profile = "/api/profile"
}
// Function using the enum
async function fetchData(endpoint: ApiEndpoint): Promise<any> {
console.log(`Fetching from: ${endpoint}`);
// In real code: const response = await fetch(endpoint);
return {};
}
// Type-safe API calls
fetchData(ApiEndpoint.Users);
fetchData(ApiEndpoint.Products);
// This would cause an error:
// fetchData("/api/wrong"); // ❌ Error: string not assignable to ApiEndpoint
// Building URLs
function buildApiUrl(endpoint: ApiEndpoint, id?: number): string {
const baseUrl = "https://api.example.com";
if (id !== undefined) {
return `${baseUrl}${endpoint}/${id}`;
}
return `${baseUrl}${endpoint}`;
}
console.log(buildApiUrl(ApiEndpoint.Users)); // "https://api.example.com/api/users"
console.log(buildApiUrl(ApiEndpoint.Products, 5)); // "https://api.example.com/api/products/5"Recommendation: Use string enums for most cases. They're more readable in logs, debugging tools, and API responses. The actual string value is clear without looking up the enum definition.
Heterogeneous Enums
Enums can mix string and numeric values, though this is rarely needed.
// Mixed string and number enum (not recommended)
enum Mixed {
No = 0,
Yes = "YES"
}
console.log(Mixed.No); // 0
console.log(Mixed.Yes); // "YES"
// Practical example: Response codes
enum ApiResponse {
Success = 200,
Error = "ERROR",
Unauthorized = 401,
NotFound = "NOT_FOUND"
}
// This works but is confusing
let response1: ApiResponse = ApiResponse.Success; // 200
let response2: ApiResponse = ApiResponse.Error; // "ERROR"
let response3: ApiResponse = ApiResponse.Unauthorized; // 401Avoid heterogeneous enums unless you have a specific reason. They're confusing and harder to maintain. Stick to either all numbers or all strings.
Const Enums
Const enums are completely removed during compilation, resulting in more optimized code.
// Regular enum
enum RegularDirection {
North,
South,
East,
West
}
// Const enum
const enum ConstDirection {
North,
South,
East,
West
}
// Using regular enum
let dir1: RegularDirection = RegularDirection.North;
// Using const enum
let dir2: ConstDirection = ConstDirection.North;
// After compilation:
// Regular enum creates an object in JavaScript
// Const enum is replaced with the actual value (0)
console.log(dir1); // Compiled: console.log(RegularDirection.North);
console.log(dir2); // Compiled: console.log(0);When to Use Const Enums
- Performance-critical code where every byte matters
- Internal APIs not exposed to external code
- Build-time constants that won't change
Const Enum Limitations
- Cannot use reverse mapping (no numeric to string lookup)
- Cannot iterate over enum members
- Cannot be used in certain advanced scenarios
⚠️ Const Enum Caution
For most applications, regular enums are better. Only use const enums if you're certain you need the performance optimization and understand the limitations.
Computed and Constant Members
Constant Enum Members
// Constant members - evaluated at compile time
enum FileAccess {
None = 0,
Read = 1 << 0, // Bitwise left shift: 1
Write = 1 << 1, // Bitwise left shift: 2
ReadWrite = Read | Write, // Bitwise OR: 3
Execute = 1 << 2 // Bitwise left shift: 4
}
console.log(FileAccess.None); // 0
console.log(FileAccess.Read); // 1
console.log(FileAccess.Write); // 2
console.log(FileAccess.ReadWrite); // 3
console.log(FileAccess.Execute); // 4
// Check permissions
function hasPermission(userAccess: FileAccess, required: FileAccess): boolean {
return (userAccess & required) === required;
}
let userPermissions: FileAccess = FileAccess.ReadWrite;
console.log(hasPermission(userPermissions, FileAccess.Read)); // true
console.log(hasPermission(userPermissions, FileAccess.Execute)); // falseComputed Enum Members
// Computed members - evaluated at runtime
function getValue(): number {
return 100;
}
enum Mixed {
A = getValue(), // Computed
B, // Error! Needs initializer after computed member
C = 200, // Constant
D // 201 (auto-increment from constant)
}
// Better: Keep it simple
enum Simple {
A = 100,
B = 200,
C = 300
}Enums at Runtime
Unlike type aliases which are erased during compilation, enums create actual JavaScript objects.
// Enum in TypeScript
enum Color {
Red = "RED",
Green = "GREEN",
Blue = "BLUE"
}
// Compiled JavaScript (simplified)
// var Color;
// (function (Color) {
// Color["Red"] = "RED";
// Color["Green"] = "GREEN";
// Color["Blue"] = "BLUE";
// })(Color || (Color = {}));
// You can iterate over enum values
function getAllColors(): string[] {
return Object.values(Color);
}
console.log(getAllColors()); // ["RED", "GREEN", "BLUE"]
// Get all enum keys
function getAllColorNames(): string[] {
return Object.keys(Color);
}
console.log(getAllColorNames()); // ["Red", "Green", "Blue"]
// Check if value is valid enum member
function isValidColor(value: string): value is Color {
return Object.values(Color).includes(value as Color);
}
console.log(isValidColor("RED")); // true
console.log(isValidColor("YELLOW")); // falsePractical Examples
Example 1: Order Management System
// Order status enum
enum OrderStatus {
Pending = "PENDING",
Confirmed = "CONFIRMED",
Processing = "PROCESSING",
Shipped = "SHIPPED",
Delivered = "DELIVERED",
Cancelled = "CANCELLED",
Refunded = "REFUNDED"
}
// Payment method enum
enum PaymentMethod {
Cash = "CASH",
Card = "CARD",
BankTransfer = "BANK_TRANSFER",
MobileMoney = "MOBILE_MONEY"
}
// Order type
type Order = {
id: number;
customerId: number;
status: OrderStatus;
paymentMethod: PaymentMethod;
totalAmount: number;
createdAt: Date;
};
// Create order
function createOrder(
customerId: number,
totalAmount: number,
paymentMethod: PaymentMethod
): Order {
return {
id: Date.now(),
customerId,
status: OrderStatus.Pending,
paymentMethod,
totalAmount,
createdAt: new Date()
};
}
// Update order status
function updateOrderStatus(order: Order, newStatus: OrderStatus): void {
console.log(`Updating order ${order.id} from ${order.status} to ${newStatus}`);
order.status = newStatus;
}
// Check if order can be cancelled
function canCancelOrder(order: Order): boolean {
return order.status === OrderStatus.Pending ||
order.status === OrderStatus.Confirmed;
}
// Usage
let order = createOrder(101, 50000, PaymentMethod.Card);
console.log(`Order created: ${order.id}`);
console.log(`Status: ${order.status}`);
console.log(`Payment: ${order.paymentMethod}`);
updateOrderStatus(order, OrderStatus.Confirmed);
updateOrderStatus(order, OrderStatus.Processing);
console.log(`Can cancel? ${canCancelOrder(order)}`); // falseExample 2: User Permission System
// User roles
enum UserRole {
SuperAdmin = "SUPER_ADMIN",
Admin = "ADMIN",
Moderator = "MODERATOR",
Editor = "EDITOR",
Viewer = "VIEWER",
Guest = "GUEST"
}
// Permissions
enum Permission {
CreateUser = "CREATE_USER",
DeleteUser = "DELETE_USER",
EditUser = "EDIT_USER",
ViewUser = "VIEW_USER",
CreatePost = "CREATE_POST",
DeletePost = "DELETE_POST",
EditPost = "EDIT_POST",
ViewPost = "VIEW_POST"
}
// Role permissions mapping
const rolePermissions: Record<UserRole, Permission[]> = {
[UserRole.SuperAdmin]: [
Permission.CreateUser,
Permission.DeleteUser,
Permission.EditUser,
Permission.ViewUser,
Permission.CreatePost,
Permission.DeletePost,
Permission.EditPost,
Permission.ViewPost
],
[UserRole.Admin]: [
Permission.CreateUser,
Permission.EditUser,
Permission.ViewUser,
Permission.CreatePost,
Permission.DeletePost,
Permission.EditPost,
Permission.ViewPost
],
[UserRole.Moderator]: [
Permission.DeletePost,
Permission.EditPost,
Permission.ViewPost,
Permission.ViewUser
],
[UserRole.Editor]: [
Permission.CreatePost,
Permission.EditPost,
Permission.ViewPost
],
[UserRole.Viewer]: [
Permission.ViewPost,
Permission.ViewUser
],
[UserRole.Guest]: [
Permission.ViewPost
]
};
// Check if user has permission
function hasPermission(role: UserRole, permission: Permission): boolean {
return rolePermissions[role].includes(permission);
}
// Usage
let adminRole: UserRole = UserRole.Admin;
let editorRole: UserRole = UserRole.Editor;
console.log(`Admin can delete users? ${hasPermission(adminRole, Permission.DeleteUser)}`);
// false
console.log(`Admin can create posts? ${hasPermission(adminRole, Permission.CreatePost)}`);
// true
console.log(`Editor can delete posts? ${hasPermission(editorRole, Permission.DeletePost)}`);
// falseExample 3: HTTP Request Handler
// HTTP methods
enum HttpMethod {
GET = "GET",
POST = "POST",
PUT = "PUT",
PATCH = "PATCH",
DELETE = "DELETE"
}
// HTTP status codes
enum HttpStatusCode {
OK = 200,
Created = 201,
NoContent = 204,
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404,
InternalServerError = 500
}
// Content types
enum ContentType {
JSON = "application/json",
FormData = "application/x-www-form-urlencoded",
MultipartFormData = "multipart/form-data",
PlainText = "text/plain"
}
// Request type
type HttpRequest = {
method: HttpMethod;
url: string;
headers: Record<string, string>;
body?: any;
};
// Response type
type HttpResponse = {
statusCode: HttpStatusCode;
contentType: ContentType;
body: any;
};
// Create request
function createRequest(
method: HttpMethod,
url: string,
body?: any
): HttpRequest {
return {
method,
url,
headers: {
"Content-Type": ContentType.JSON
},
body
};
}
// Handle request
function handleRequest(request: HttpRequest): HttpResponse {
console.log(`${request.method} ${request.url}`);
if (request.method === HttpMethod.GET) {
return {
statusCode: HttpStatusCode.OK,
contentType: ContentType.JSON,
body: { message: "Data retrieved" }
};
}
if (request.method === HttpMethod.POST) {
return {
statusCode: HttpStatusCode.Created,
contentType: ContentType.JSON,
body: { message: "Resource created" }
};
}
return {
statusCode: HttpStatusCode.BadRequest,
contentType: ContentType.JSON,
body: { error: "Invalid method" }
};
}
// Usage
let getRequest = createRequest(HttpMethod.GET, "/api/users");
let postRequest = createRequest(HttpMethod.POST, "/api/users", { name: "Yusuf" });
let getResponse = handleRequest(getRequest);
let postResponse = handleRequest(postRequest);
console.log(`GET Response: ${getResponse.statusCode} - ${JSON.stringify(getResponse.body)}`);
console.log(`POST Response: ${postResponse.statusCode} - ${JSON.stringify(postResponse.body)}`);Enums vs Alternatives
Enums vs Union Types
// Using Enum
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
Pending = "PENDING"
}
let status1: Status = Status.Active;
// Using Union Type
type StatusUnion = "ACTIVE" | "INACTIVE" | "PENDING";
let status2: StatusUnion = "ACTIVE";
// Enum advantages:
// - Autocomplete suggests all values
// - Can iterate over values
// - Clear namespace (Status.Active)
// - Runtime object exists
// Union advantages:
// - Simpler, no extra code generated
// - Works directly with strings
// - No compilation overheadEnums vs Object as Const
// Using Enum
enum Color {
Red = "RED",
Green = "GREEN",
Blue = "BLUE"
}
// Using Object as Const
const ColorObj = {
Red: "RED",
Green: "GREEN",
Blue: "BLUE"
} as const;
type ColorType = typeof ColorObj[keyof typeof ColorObj];
// Both work similarly
let enumColor: Color = Color.Red;
let objColor: ColorType = ColorObj.Red;
// Enum: Clearer syntax, built-in TypeScript feature
// Object: More flexible, no extra JavaScript generatedTest Your Knowledge
Type Safety Check
Which enum usage is correct?
enum Direction {
North,
South,
East,
West
}
// Option A
let direction1: Direction = Direction.North;
// Option B
let direction2: Direction = "North";
// Option C
let direction3: Direction = 0;
// Option D
let direction4 = Direction.East;Best Practices
- Use string enums for most cases—they're more readable
- Use PascalCase for enum names (e.g.,
OrderStatus) - Use PascalCase for enum members (e.g.,
OrderStatus.Pending) - Avoid heterogeneous enums—stick to all strings or all numbers
- Use const enums sparingly—only when you need the optimization
- Group related constants into a single enum
- Consider union types for simple cases
- Document enum purpose with comments for complex enums
// ✅ Good: String enum with clear values
enum OrderStatus {
Pending = "PENDING",
Processing = "PROCESSING",
Completed = "COMPLETED",
Cancelled = "CANCELLED"
}
// ✅ Good: Grouped related constants
enum PaymentMethod {
Cash = "CASH",
Card = "CARD",
BankTransfer = "BANK_TRANSFER",
MobileMoney = "MOBILE_MONEY"
}
// ❌ Avoid: Heterogeneous enum
enum BadEnum {
First = 1,
Second = "SECOND" // Mixing types
}
// ❌ Avoid: Unclear abbreviations
enum Stat {
P = "P",
C = "C",
A = "A"
}
// ✅ Better: Clear, descriptive names
enum Status {
Pending = "PENDING",
Completed = "COMPLETED",
Active = "ACTIVE"
}Key Takeaways
- Enums define a set of named constants for related values
- Numeric enums auto-increment from 0 (or custom start value)
- String enums are more readable and better for most use cases
- Enums provide type safety, autocomplete, and prevent typos
- Numeric enums have reverse mapping (value → name lookup)
- Const enums are optimized away during compilation
- Enums create runtime objects (unlike type aliases)
- Use enums for status codes, user roles, states, and categories
- Consider union types as simpler alternative for basic cases
- Use PascalCase for both enum names and members
What's Next?
Now that you've mastered enums and all the basic types, you're ready to explore Any, Unknown, and Never! In the next lesson, you'll learn about TypeScript's special types—when to use them safely, how they differ from each other, and why they're important for handling edge cases. These advanced types will complete your understanding of TypeScript's type system.
Get ready to handle uncertainty and impossibility with TypeScript's special types!