Skip to main content
Advertisement

2.4 Enums

An enum (enumeration) is a way of giving meaningful names to a set of related constants. For example, expressing directions as Direction.Up, Direction.Down, Direction.Left, Direction.Right instead of 0, 1, 2, 3 greatly improves code readability and safety. TypeScript supports numeric enums, string enums, const enums, and heterogeneous enums, each with a different purpose and compiled output.

However, enums are TypeScript-specific syntax that does not exist in JavaScript, so they generate runtime objects and are difficult to tree-shake. This chapter covers every form of enum and also addresses the modern as const object pattern as an alternative.


Numeric Enums

When no values are specified during declaration, members start at 0 and auto-increment by 1.

enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}

const move: Direction = Direction.Up;
console.log(move); // 0
console.log(Direction[0]); // "Up" — reverse mapping
console.log(Direction["Up"]); // 0

Specifying Initial Values

When you assign an initial value to a specific member, subsequent members increment from that value.

enum HttpStatus {
// 2xx Success
OK = 200,
Created, // 201
Accepted, // 202
NoContent = 204,

// 4xx Client Errors
BadRequest = 400,
Unauthorized, // 401
Forbidden, // 403 — this is wrong! it skips 402
NotFound = 404,

// 5xx Server Errors
InternalServerError = 500,
BadGateway = 502,
ServiceUnavailable = 503,
}

console.log(HttpStatus.OK); // 200
console.log(HttpStatus.Created); // 201
console.log(HttpStatus.NotFound); // 404

Reverse Mapping

After compilation, numeric enums generate a bidirectional mapping object. You can get a value from a name, or a name from a value.

enum Status {
Pending = 1,
Active, // 2
Inactive, // 3
}

console.log(Status.Active); // 2 — name → value
console.log(Status[2]); // "Active" — value → name (reverse mapping)

// Compiled JavaScript (for illustration)
// var Status;
// (function (Status) {
// Status[Status["Pending"] = 1] = "Pending";
// Status[Status["Active"] = 2] = "Active";
// Status[Status["Inactive"] = 3] = "Inactive";
// })(Status || (Status = {}));

Reverse mapping is useful for converting numeric codes to human-readable names during debugging.

function describeStatus(code: number): string {
const name = Status[code];
return name ?? `Unknown status (${code})`;
}

console.log(describeStatus(2)); // "Active"
console.log(describeStatus(99)); // "Unknown status (99)"

String Enums

Each member is initialized with an explicit string value. There is no auto-increment, so every member must be assigned a value directly.

enum HttpMethod {
Get = "GET",
Post = "POST",
Put = "PUT",
Patch = "PATCH",
Delete = "DELETE",
}

const method: HttpMethod = HttpMethod.Post;
console.log(method); // "POST" — human-readable value

Advantages of String Enums

  1. Serialization safety: The meaningful string is used as-is when converting to JSON or passing to an API.
  2. Debugging readability: "POST" shows up in logs instead of 2, so the meaning is immediately clear.
  3. No reverse mapping: String enums do not generate reverse mappings, keeping the runtime object smaller.
enum LogLevel {
Debug = "DEBUG",
Info = "INFO",
Warn = "WARN",
Error = "ERROR",
Fatal = "FATAL",
}

function log(level: LogLevel, message: string): void {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${level}] ${message}`);
// [2026-03-21T00:00:00.000Z] [WARN] Disk usage is high
}

log(LogLevel.Warn, "Disk usage is high");

const Enum — Inline Substitution After Compilation

A regular enum remains as a JavaScript object at runtime after compilation. A const enum inlines all member references with their actual values at compile time and removes the object itself. This is an optimization technique that reduces bundle size and eliminates runtime access overhead.

const enum Color {
Red = 0,
Green = 1,
Blue = 2,
}

// Usage code
const myColor = Color.Green;
const isRed = myColor === Color.Red;

Compiled JavaScript:

// The Color object itself is not created
const myColor = 1 /* Color.Green */;
const isRed = myColor === 0 /* Color.Red */;

Constraints of const enum

const enum Permission {
Read = 1,
Write = 2,
Execute = 4,
}

// Dynamic access is not allowed — the object doesn't exist
// Permission[1]; // error: A const enum member can only be accessed using a string literal

// Using as array index — OK (inlined at compile time)
const perms: number[] = [Permission.Read, Permission.Write];

// In environments with isolatedModules: true (Babel, esbuild, etc.), const enum cannot be used
// Tools that compile each file independently cannot reference const enum values from other files

Heterogeneous Enums

An enum that mixes numeric and string values. Rarely used in practice and not recommended.

enum Mixed {
No = 0,
Yes = "YES",
}

// Usable, but confusing and hard to maintain
// In most cases, it's better to choose either a numeric or string enum

Downsides of Enums

1. Tree-shaking Difficulties

Modern bundlers (Webpack, Rollup, esbuild) perform tree-shaking to remove unused code. However, regular enums compile to an immediately invoked function expression (IIFE) pattern, making it difficult for bundlers to statically analyze whether they are used. As a result, modules containing enums may be included in the bundle in their entirety.

// Compiled enum — IIFE pattern is hard for bundlers to analyze
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
Direction[Direction["Left"] = 2] = "Left";
Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));

2. Weak Type Safety in Numeric Enums

Numeric enums allow any number to be assigned, even values outside the defined range.

enum Direction { Up = 0, Down = 1, Left = 2, Right = 3 }

function move(dir: Direction): void {
console.log(dir);
}

move(Direction.Up); // OK
move(99); // OK — should be an error, but TypeScript allows it!
// All numbers are assignable to Direction (a design flaw)

String enums do not have this problem.

enum HttpMethod { Get = "GET", Post = "POST" }

function request(method: HttpMethod): void {}

request(HttpMethod.Get); // OK
request("GET"); // error: Argument of type '"GET"' is not assignable to parameter of type 'HttpMethod'

3. Incompatibility with the JavaScript Ecosystem

Enums are TypeScript-specific syntax. Tools that transform TypeScript to JavaScript by simply stripping types (e.g., @babel/plugin-transform-typescript, the default mode of esbuild) cannot handle const enum.


as const Objects — The Modern Alternative to Enums

The as const object pattern behaves like a regular JavaScript object at runtime while providing type-level safety equivalent to an enum. It also works well with tree-shaking and has no problems in isolatedModules environments.

// Traditional enum approach
enum DirectionEnum {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}

// as const alternative
const Direction = {
Up: "UP",
Down: "DOWN",
Left: "LEFT",
Right: "RIGHT",
} as const;

// Extract the type
type Direction = typeof Direction[keyof typeof Direction];
// Direction = "UP" | "DOWN" | "LEFT" | "RIGHT"

Type Extraction Pattern in Detail

const Direction = {
Up: "UP",
Down: "DOWN",
Left: "LEFT",
Right: "RIGHT",
} as const;

// keyof typeof Direction = "Up" | "Down" | "Left" | "Right" (key union)
// typeof Direction[keyof typeof Direction] = "UP" | "DOWN" | "LEFT" | "RIGHT" (value union)
type DirectionType = typeof Direction[keyof typeof Direction];

function move(dir: DirectionType): void {
console.log(`Moving ${dir}`);
}

move(Direction.Up); // OK: "UP"
move("UP"); // OK: string literals are also allowed
move("DIAGONAL"); // error: Argument of type '"DIAGONAL"' is not assignable

// All values can be enumerated at runtime
const allDirections = Object.values(Direction);
// ["UP", "DOWN", "LEFT", "RIGHT"]

Numeric Values with as const Pattern

const HttpStatus = {
OK: 200,
Created: 201,
NoContent: 204,
BadRequest: 400,
Unauthorized: 401,
NotFound: 404,
InternalServerError: 500,
} as const;

type HttpStatusCode = typeof HttpStatus[keyof typeof HttpStatus];
// 200 | 201 | 204 | 400 | 401 | 404 | 500

function handleResponse(status: HttpStatusCode): void {
switch (status) {
case HttpStatus.OK:
console.log("Success");
break;
case HttpStatus.NotFound:
console.log("Resource not found");
break;
default:
console.log(`Status code: ${status}`);
}
}

Practical Example 1: HTTP Status Codes and Directions

// String enum — API methods
enum HttpMethod {
Get = "GET",
Post = "POST",
Put = "PUT",
Patch = "PATCH",
Delete = "DELETE",
}

interface ApiRequest {
url: string;
method: HttpMethod;
body?: unknown;
}

async function apiCall(request: ApiRequest): Promise<unknown> {
const response = await fetch(request.url, {
method: request.method,
body: request.body ? JSON.stringify(request.body) : undefined,
headers: { "Content-Type": "application/json" },
});
return response.json();
}

// Usage
const req: ApiRequest = {
url: "/api/users/1",
method: HttpMethod.Get,
};

// Game direction — const enum (bundle size optimization)
const enum GameDirection {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}

interface Player {
x: number;
y: number;
}

function movePlayer(player: Player, dir: GameDirection): Player {
switch (dir) {
case GameDirection.Up: return { ...player, y: player.y - 1 };
case GameDirection.Down: return { ...player, y: player.y + 1 };
case GameDirection.Left: return { ...player, x: player.x - 1 };
case GameDirection.Right: return { ...player, x: player.x + 1 };
}
}

Practical Example 2: Role System

// Define roles with as const pattern
const Role = {
Guest: "GUEST",
User: "USER",
Moderator: "MODERATOR",
Admin: "ADMIN",
SuperAdmin: "SUPER_ADMIN",
} as const;

type RoleType = typeof Role[keyof typeof Role];

const RoleLevel: Record<RoleType, number> = {
[Role.Guest]: 0,
[Role.User]: 1,
[Role.Moderator]: 2,
[Role.Admin]: 3,
[Role.SuperAdmin]: 4,
};

function hasPermission(userRole: RoleType, requiredRole: RoleType): boolean {
return RoleLevel[userRole] >= RoleLevel[requiredRole];
}

function requireRole(userRole: RoleType, requiredRole: RoleType): void {
if (!hasPermission(userRole, requiredRole)) {
throw new Error(
`Insufficient permission: ${requiredRole} or higher required, current role is ${userRole}`
);
}
}

// Usage
const currentUser = { name: "Alice", role: Role.Moderator };

requireRole(currentUser.role, Role.User); // OK
requireRole(currentUser.role, Role.Moderator); // OK
// requireRole(currentUser.role, Role.Admin); // Error: Insufficient permission

Practical Example 3: Bitflag Permissions

// Expressing bitflags with numeric enum
enum Permission {
None = 0,
Read = 1 << 0, // 1
Write = 1 << 1, // 2
Execute = 1 << 2, // 4
Delete = 1 << 3, // 8
Admin = Read | Write | Execute | Delete, // 15
}

function hasFlag(permissions: number, flag: Permission): boolean {
return (permissions & flag) === flag;
}

function addFlag(permissions: number, flag: Permission): number {
return permissions | flag;
}

function removeFlag(permissions: number, flag: Permission): number {
return permissions & ~flag;
}

// Combining user permissions
let userPerms = Permission.Read | Permission.Write; // 3 (0b0011)

console.log(hasFlag(userPerms, Permission.Read)); // true
console.log(hasFlag(userPerms, Permission.Execute)); // false

userPerms = addFlag(userPerms, Permission.Execute); // 7 (0b0111)
console.log(hasFlag(userPerms, Permission.Execute)); // true

userPerms = removeFlag(userPerms, Permission.Write); // 5 (0b0101)
console.log(hasFlag(userPerms, Permission.Write)); // false

Pro Tips

Tip 1: Prefer as const + type extraction over string enums

// String enum — incompatible with external string values
enum Status { Active = "ACTIVE" }
const s: Status = "ACTIVE"; // error!

// as const — compatible with string literals
const Status = { Active: "ACTIVE" } as const;
type Status = typeof Status[keyof typeof Status];
const s: Status = "ACTIVE"; // OK

Tip 2: Be mindful of where const enum is declared

// const enum must be declared in the same file or a d.ts file
// In isolatedModules environments, const enum cannot be imported and used from outside
// Depending on team settings, replacing with a regular enum or as const is safer

Tip 3: Use exhaustive checks with never for enum completeness

const enum Season { Spring, Summer, Autumn, Winter }

function describe(s: Season): string {
switch (s) {
case Season.Spring: return "Spring";
case Season.Summer: return "Summer";
case Season.Autumn: return "Autumn";
case Season.Winter: return "Winter";
default: {
const _exhaustive: never = s; // never when all cases are handled
return _exhaustive;
}
}
}

Tip 4: Validate enum values at runtime

enum Color { Red = "RED", Green = "GREEN", Blue = "BLUE" }

function isColor(value: string): value is Color {
return Object.values(Color).includes(value as Color);
}

const input = "RED";
if (isColor(input)) {
const color: Color = input; // type guard passed
console.log(color); // Color.Red
}

Tip 5: Use Map or Record instead of enum for large constant sets

// For large constant sets like hundreds of error codes, Record is more flexible than objects
const ERROR_MESSAGES: Record<number, string> = {
1000: "Authentication failed",
1001: "Token expired",
1002: "Insufficient permissions",
// ...
};

function getErrorMessage(code: number): string {
return ERROR_MESSAGES[code] ?? `Unknown error (${code})`;
}

Comparison Table

CategoryNumeric EnumString Enumconst Enumas const Object
Auto-incrementOXO (numeric)X
Reverse mappingOXXX
Serialization readabilityLowHighLowHigh
Compiled outputJS objectJS objectInlinedJS object
Tree-shakingDifficultDifficultFully removedEasy
Type safetyPartialFullFullFull
isolatedModulesCompatibleCompatibleIncompatibleCompatible
Recommended forBit flagsWhen enum is neededPerformance optimizationMost cases

The next chapter takes a deep dive into TypeScript's type inference system. We will learn about variable initialization, function return values, contextual typing, type widening and narrowing, and how to control inference precisely with as const.

Advertisement