cheat sheet

Enums

TypeScript enums create named constant sets as real runtime objects. Covers numeric, string, and const enums; reverse mapping; enum pitfalls; and when to prefer a union of string literals instead.

#typescript#enums#languageupdated 04-26-2026

Enums

What it is

Enums are a TypeScript-specific construct that creates named constant sets. Unlike most TypeScript features that are erased at compile time, enums compile to real JavaScript objects — they have both a type (used in type annotations) and a runtime value (an object you can use in code). Use enums when you have a fixed, known set of named values and want them to exist as a real runtime object.

Numeric enums

Numeric enums auto-assign integer values starting from 0. Members without an explicit value are incremented from the previous member.

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

const move = Direction.Up; // 0
console.log(move);         // 0
console.log(Direction.Up); // 0

Output:

text
0
0

Explicit values — subsequent members increment from the last explicit value:

typescript
enum HttpStatus {
  Ok      = 200,
  Created = 201,
  NoContent = 204,
  BadRequest = 400,
  Unauthorized, // 401
  Forbidden,    // 402 — auto-incremented
  NotFound = 404,
  InternalServerError = 500,
}

const status: HttpStatus = HttpStatus.Ok;
console.log(status); // 200

Output:

text
200

Reverse mapping

Numeric enums generate a reverse mapping from value to name — a feature unique to numeric enums:

typescript
enum Direction {
  Up = 0,
  Down = 1,
}

console.log(Direction[0]);         // "Up"
console.log(Direction["Up"]);      // 0
console.log(Direction[Direction.Up]); // "Up"

Output:

text
Up
0
Up

The compiled JavaScript for a numeric enum shows the reverse mapping:

javascript
var Direction;
(function (Direction) {
  Direction[Direction["Up"] = 0] = "Up";
  Direction[Direction["Down"] = 1] = "Down";
})(Direction || (Direction = {}));

String enums

String enums require explicit values for every member. There is no reverse mapping for string enums.

typescript
enum Status {
  Pending   = "PENDING",
  Active    = "ACTIVE",
  Inactive  = "INACTIVE",
  Deleted   = "DELETED",
}

const s: Status = Status.Active;
console.log(s); // "ACTIVE"

// String enums do NOT have reverse mapping
// Status["ACTIVE"] → undefined (not "Active")

Output:

text
ACTIVE

String enums are often preferred over numeric enums because their values are human-readable in logs, network responses, and databases.

typescript
function updateUserStatus(userId: string, status: Status): void {
  console.log(`Setting user ${userId} to ${status}`);
  // Output: "Setting user 42 to ACTIVE"
  // Much clearer than: "Setting user 42 to 1"
}

Const enums

const enum values are inlined at compile time — no runtime object is emitted. This eliminates the JavaScript overhead of the enum object entirely.

typescript
const enum Direction {
  Up,
  Down,
  Left,
  Right,
}

const d = Direction.Up;

Compiled output — the value is directly inlined:

javascript
const d = 0 /* Direction.Up */;

No Direction object exists at runtime. This means:

  • No reverse mapping available
  • Cannot iterate over enum members at runtime
  • Cannot use Object.keys(Direction) or Object.values(Direction)

Avoid const enum in published library .d.ts files. When a consumer compiles their code, TypeScript inlines the numeric values from your declaration file. If you later change the enum values in a new version of your library, consumers' compiled code will silently use the old inlined values without recompiling. Use a regular enum or a union of string literals in libraries instead.

Heterogeneous enums (avoid)

TypeScript allows mixing numeric and string values in a single enum. This is generally considered a code smell and should be avoided.

typescript
// Avoid — confusing and rarely useful
enum Mixed {
  No  = 0,
  Yes = "YES",
}

Computed and constant members

Members can use constant expressions or computed values (though computed members are rare):

typescript
enum FileAccess {
  None,                    // 0 — constant
  Read    = 1 << 1,        // 2 — constant expression
  Write   = 1 << 2,        // 4 — constant expression
  ReadWrite = Read | Write, // 6 — constant expression
}

// Bitwise flag checking
function canRead(access: FileAccess): boolean {
  return (access & FileAccess.Read) !== 0;
}

console.log(canRead(FileAccess.ReadWrite)); // true
console.log(canRead(FileAccess.Write));     // false

Output:

text
true
false

Using enums as types

Enum names serve as types in function signatures:

typescript
enum LogLevel {
  Debug   = "DEBUG",
  Info    = "INFO",
  Warning = "WARNING",
  Error   = "ERROR",
}

function log(level: LogLevel, message: string): void {
  const timestamp = new Date().toISOString();
  console.log(`[${timestamp}] [${level}] ${message}`);
}

log(LogLevel.Info, "Server started");
log(LogLevel.Error, "Connection refused");

// Error — string literal is not assignable to LogLevel even though the values match
log("INFO", "test"); // TS2345: Argument of type '"INFO"' is not assignable to parameter of type 'LogLevel'

This last point is a key difference from union of string literals: enum types are nominal (only exact enum members are accepted), while string literal unions are structural (matching strings are accepted). This makes enums stricter but less convenient to use from external inputs like API responses.

Iterating over enum members

Use Object.keys, Object.values, and Object.entries on the runtime enum object to loop over its members. Note that numeric enums include both the name strings and the reverse-mapped numeric keys, so filter with isNaN when you only want names.

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

// Get all keys
const colorKeys = Object.keys(Color) as Array<keyof typeof Color>;
// ["Red", "Green", "Blue"]

// Get all values
const colorValues = Object.values(Color);
// ["RED", "GREEN", "BLUE"]

// Get the type of enum keys
type ColorKey = keyof typeof Color; // "Red" | "Green" | "Blue"

// Iterate
for (const key of colorKeys) {
  console.log(`${key} = ${Color[key]}`);
}

Output:

text
Red = RED
Green = GREEN
Blue = BLUE

For numeric enums, Object.keys includes both names and values due to reverse mapping. Cast to avoid:

typescript
enum Direction { Up = 0, Down = 1 }

// Includes "0", "1" in addition to "Up", "Down"
Object.keys(Direction); // ["0", "1", "Up", "Down"]

// Filter to only string keys (names)
const names = Object.keys(Direction).filter((k) => isNaN(Number(k)));
// ["Up", "Down"]

Pitfalls

Numeric enum widening

Numeric enums accept any number, not just the defined members. This is a known design flaw.

typescript
enum Status { Active = 1, Inactive = 2 }

function setStatus(s: Status) { /* ... */ }

setStatus(Status.Active); // OK
setStatus(99);            // OK — TypeScript allows this! No error.
setStatus(1);             // OK — even though 1 is not explicitly named here

String enums do not have this problem — only exact enum members are accepted.

Ordering bugs in auto-incremented enums

Adding a member in the middle of an auto-incremented numeric enum shifts all subsequent values:

typescript
// v1
enum Priority { Low = 0, High = 1 }

// v2 — inserted Medium in the middle
enum Priority { Low = 0, Medium = 1, High = 2 }
// High is now 2 instead of 1 — any stored numeric values are now wrong

Always use explicit numeric values or string enums to avoid ordering bugs.

const enum in declaration files

As noted above, const enum in .d.ts files causes silent value-mismatch bugs across versions. TypeScript 5.0+ warns about this with isolatedModules.

Enum vs union of string literals

For many use cases, a union of string literals is simpler and safer than an enum:

typescript
// Enum approach
enum Status {
  Pending  = "PENDING",
  Active   = "ACTIVE",
  Inactive = "INACTIVE",
}
function getStatus(): Status { return Status.Active; }

// Union of string literals approach
type Status = "PENDING" | "ACTIVE" | "INACTIVE";
function getStatus(): Status { return "ACTIVE"; }
FeatureenumString literal union
Runtime objectYesNo
Tree-shakeableNoYes
Can iterate membersYesNo (without array)
Nominal typingYes (stricter)No (structural)
Accepts raw stringNo (must use enum member)Yes
Works with JSON APIsAwkwardNatural
Requires importYesOnly type import
Declaration mergingNoVia type intersection

When to prefer enum:

  • You need to iterate over the members at runtime
  • You need a runtime dictionary (reverse mapping, Object.entries)
  • You want strict nominal typing that rejects raw strings even with matching values
  • You are working with a framework that relies on enums (e.g. TypeORM column type)

When to prefer string literal union:

  • The values come from or go to an external API or database
  • You want tree-shakeable code
  • You want to accept matching string values without wrapping them in an enum member call
  • You are authoring a library where consumers should not need to import your enum to use the values

Enum vs as const object — the modern recommendation

as const on a plain object gives you the literal union type and the runtime object, with none of the enum-specific quirks. The official TypeScript team guidance (post-TS 5.0) leans toward this pattern for new code unless you have a specific need for enums.

typescript
// Modern: as const object
const Status = {
  Pending:  "PENDING",
  Active:   "ACTIVE",
  Inactive: "INACTIVE",
} as const;

type Status = (typeof Status)[keyof typeof Status];
// type Status = "PENDING" | "ACTIVE" | "INACTIVE"

function setStatus(s: Status) { console.log(s) }

setStatus(Status.Active);  // OK
setStatus("ACTIVE");       // OK — accepts matching string literal
// setStatus("XYZ");       // Error — not in the union

// Iterate just like an enum
for (const [name, value] of Object.entries(Status)) {
  console.log(`${name} = ${value}`);
}

Output:

text
ACTIVE
Pending = PENDING
Active = ACTIVE
Inactive = INACTIVE

The advantages over enum:

  • Tree-shakeable — unused members are dropped by bundlers. Enums always emit the full runtime object.
  • No reverse mapping noise — numeric enums add string-to-name entries which break naive Object.keys iteration.
  • Accepts matching string literals — natural interop with JSON, REST APIs, and database columns.
  • No keyword reservationenum is a TypeScript-specific keyword that ESM-only modes (isolatedModules, verbatimModuleSyntax) have special rules for. Plain objects have none.
  • Type and value are independenttype Status and const Status use the same identifier in two namespaces, which mirrors the enum behaviour without the magic.

The single capability you lose is nominal typing — an as const union accepts any matching string, while an enum only accepts members. For most application code that is a feature, not a bug.

See the satisfies article for the as const satisfies variant that adds shape checking to this pattern.

Const enum and isolatedModules

const enum is the most fraught corner of TypeScript enums. The compiler inlines const-enum members into the call site at compile time — there is no runtime object — which makes them fast but breaks key tooling.

typescript
const enum Direction {
  Up = "UP",
  Down = "DOWN",
}

const dir = Direction.Up;
// Compiles to: const dir = "UP" /* Direction.Up */;

The isolatedModules conflict

isolatedModules: true (which Babel, swc, esbuild, and tsx all require) forbids const enum use across modules because each module is compiled in isolation, and a transpiler that does not see the original const enum declaration cannot inline its values. The TS compiler emits an error:

text
TS2748: Cannot access ambient const enums when 'isolatedModules' is enabled.

The fix is one of:

  1. Remove const — turn it into a regular enum. The runtime object is created, but cross-module use works.
  2. Use --preserveConstEnums with the official tsc compiler. Other transpilers still won't honour it.
  3. Switch to an as const object — the most portable option.
  4. Mark imports as type-only if you only use the type, not the values.
typescript
// shared.ts
export const enum Direction {
  Up = "UP",
  Down = "DOWN",
}

// usage.ts — works only with tsc
import { Direction } from "./shared";
console.log(Direction.Up); // tsc: "UP"; swc/esbuild: throws or errors

Declared const enums (declaration files)

A declare const enum in a .d.ts file expects consumers to inline the values. If you publish a library with declare const enums and the consumer's bundler does not honour them, they get runtime errors at import sites because no implementation was shipped.

typescript
// library.d.ts
export declare const enum Color {
  Red = "RED",
  Blue = "BLUE",
}
// No corresponding library.js file emits the Color object — it's "all type".

The TypeScript team officially recommends against declare const enum in published packages. The TS 5.0 release notes explicitly warn:

"Avoid const enum in shipped library declaration files. Consumers' bundlers may not inline values, leading to runtime crashes."

Use either a plain enum (heavier runtime, but interoperable) or a string-literal union (tree-shakeable, transparent).

ESM vs CJS impact on enum emit

Enums emit a self-IIFE pattern that works in both CommonJS and ESM but has different consequences in each.

javascript
// Enum source:
enum Direction { Up, Down }

// CJS emit (target: CommonJS):
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Direction = void 0;
var Direction;
(function (Direction) {
  Direction[Direction["Up"]   = 0] = "Up";
  Direction[Direction["Down"] = 1] = "Down";
})(Direction = exports.Direction || (exports.Direction = {}));

// ESM emit (target: ES2020+ or "module": "esnext"):
export var Direction;
(function (Direction) {
  Direction[Direction["Up"]   = 0] = "Up";
  Direction[Direction["Down"] = 1] = "Down";
})(Direction || (Direction = {}));

Notable consequences:

  • ESM emit uses export var — not export const, so the enum is mutable from the outside (consumers could overwrite members). This is rarely exploited but worth knowing.
  • Tree-shaking — ESM bundlers (Rollup, esbuild, Vite) cannot tree-shake enums because the IIFE has side effects. Even if no member is referenced, the whole object is retained.
  • Dead-code elimination of const enumsconst enum can be tree-shaken because every reference is inlined and the declaration is erased. This is one of the few cases where const enum is genuinely better than alternatives, but isolatedModules blocks it.
  • verbatimModuleSyntax — TS 5.0's strictest module-syntax flag requires import { type Direction } for type-only enum imports and outright forbids importing const enums you don't have access to declarations for.

Performance considerations

Enum performance is rarely a bottleneck, but for high-frequency code (game loops, hot render paths, parsers) the choice matters.

ConstructLookup costMemoryTree-shake
Numeric enumObject property access (~1ns)One object per enumNo
String enumObject property access (~1ns)One object per enumNo
const enumInlined literal (~0ns)NoneN/A
as const objectObject property access (~1ns)One object per declarationYes
String literal unionNo object, direct comparisonNoneN/A

Practical guidance:

  • For types, prefer string literal unions or as const objects unless you need iteration.
  • For runtime values, prefer as const over enum for tree-shaking.
  • For hot paths, const enum is the fastest — but the cross-module problem usually rules it out.

Bitwise flag enums

Numeric enums with bit shifts are the canonical pattern for flag sets — multiple independent boolean states packed into one number.

typescript
enum Permission {
  None    = 0,
  Read    = 1 << 0, // 1
  Write   = 1 << 1, // 2
  Execute = 1 << 2, // 4
  Delete  = 1 << 3, // 8
  All     = Read | Write | Execute | Delete, // 15
}

function has(perms: Permission, flag: Permission): boolean {
  return (perms & flag) === flag;
}

function grant(perms: Permission, flag: Permission): Permission {
  return perms | flag;
}

function revoke(perms: Permission, flag: Permission): Permission {
  return perms & ~flag;
}

let user: Permission = Permission.None;
user = grant(user, Permission.Read);
user = grant(user, Permission.Write);
console.log(has(user, Permission.Read));    // true
console.log(has(user, Permission.Execute)); // false
user = revoke(user, Permission.Read);
console.log(has(user, Permission.Read));    // false

Output:

text
true
false
false

The 32-bit signed-integer limit on JavaScript bitwise operators caps this pattern at 32 flags. Use bigint and explicit BigInt operators if you need more.

Numeric enum widening (covered above) is especially dangerous with flag enums — setPerms(99) would silently corrupt the flag bits. Prefer the as const object pattern with explicit branding for hardened code.

Enum with computed members

Enum members can be initialized with constant expressions (visible to the compiler at compile time) or computed values (resolved at runtime).

typescript
enum FileSize {
  Byte = 1,
  Kb   = 1024,
  Mb   = Kb * Kb,            // 1048576 — constant expression
  Gb   = Mb * Kb,            // 1073741824 — constant expression
  Random = Math.random() * 100, // computed at runtime
}

console.log(FileSize.Mb);     // 1048576
console.log(FileSize.Random); // 0.0–99.9, different each run

Output:

text
1048576
47.382691038423765

Computed members have two restrictions:

  1. They must come after all non-computed members, or the auto-incrementing logic for subsequent members fails.
  2. They cannot be used in non-numeric contexts where the compiler needs the literal value — e.g., as an indexed-access type or a keyof typeof-derived union.

For most code, stick with constant expressions.

Iteration patterns — full reference

Iterating an enum at runtime requires understanding the enum's shape. Numeric enums include reverse mappings; string enums do not.

typescript
enum Color  { Red = "RED", Green = "GREEN", Blue = "BLUE" }
enum NumDir { Up = 0, Down = 1, Left = 2, Right = 3 }

// String enum — Object.keys gives names, Object.values gives values
console.log(Object.keys(Color));   // ["Red", "Green", "Blue"]
console.log(Object.values(Color)); // ["RED", "GREEN", "BLUE"]

// Numeric enum — Object.keys gives BOTH names and reverse keys
console.log(Object.keys(NumDir));
// ["0", "1", "2", "3", "Up", "Down", "Left", "Right"]

// Filter to names only (numeric enums)
const names = Object.keys(NumDir).filter((k) => isNaN(Number(k)));
// ["Up", "Down", "Left", "Right"]

// Filter to values only (numeric enums)
const values = Object.values(NumDir).filter((v) => typeof v === "number") as number[];
// [0, 1, 2, 3]

// Type the names
type ColorName  = keyof typeof Color;  // "Red" | "Green" | "Blue"
type NumDirName = keyof typeof NumDir; // "Up" | "Down" | "Left" | "Right" — clean

Output:

text
[ 'Red', 'Green', 'Blue' ]
[ 'RED', 'GREEN', 'BLUE' ]
[ '0', '1', '2', '3', 'Up', 'Down', 'Left', 'Right' ]

Common pitfalls

  1. Numeric enum accepting any numbersetStatus(99) compiles even when 99 isn't a member. Use string enums, as const objects, or branded numbers.
  2. Reordering numeric enum breaks stored values — inserting a member shifts auto-incremented values. Always use explicit numeric values for any enum whose values are persisted.
  3. const enum with isolatedModules — forbidden across modules in Babel/swc/esbuild. Use plain enum or as const.
  4. declare const enum in published .d.ts — bundlers may not inline, causing runtime crashes for consumers. Officially discouraged.
  5. Reverse mapping confusing iterationObject.keys(numericEnum) returns names and string-coerced numbers. Filter with isNaN(Number(key)).
  6. String enum not assignable from stringsetStatus("ACTIVE") errors even though the value matches. Use as const if you need string interop.
  7. Heterogeneous enums — mixing strings and numbers is legal but produces confusing types. Avoid.
  8. Enum identifier collides with typeenum Foo declares both a type and a value named Foo. If you export type Foo only and someone tries to use Foo as a value, it fails. Be explicit.
  9. Enum members lose autocomplete after as(value as Status) widens to the enum and may bypass exhaustive checks. Prefer narrowing patterns over casts.
  10. verbatimModuleSyntax and enum imports — TS 5.0+ may require import { type Status } from "./status" for type-only enum usage. Adjust imports per file.
  11. Const enum in test files — Jest/Vitest using transformers without tsc will fail on const-enum imports. Switch to plain enum or as const for test-friendliness.
  12. Bitwise flag overflow — 32-bit signed-int limit means the 32nd flag becomes negative. Use bigint for >32 flags.

Real-world recipes

Recipe 1 — HTTP status codes as as-const

A common pattern that gives you typed status codes without enum baggage. Tree-shakeable, JSON-friendly, no reverse-mapping noise.

typescript
const HttpStatus = {
  Ok:                  200,
  Created:             201,
  NoContent:           204,
  BadRequest:          400,
  Unauthorized:        401,
  Forbidden:           403,
  NotFound:            404,
  InternalServerError: 500,
  BadGateway:          502,
} as const;

type HttpStatusName = keyof typeof HttpStatus;
type HttpStatusCode = (typeof HttpStatus)[HttpStatusName];

function respond(code: HttpStatusCode, body: unknown): void {
  console.log(`-> ${code}: ${JSON.stringify(body)}`);
}

respond(HttpStatus.Ok, { ok: true });
respond(HttpStatus.NotFound, { error: "missing" });
// respond(999, { ... }); // Error — 999 not a HttpStatusCode

Recipe 2 — String enum with typed iteration

When you genuinely need an enum (e.g., for a TypeORM column type, GraphQL enum, or a framework that introspects enums at runtime), use the strictest typing patterns to compensate.

typescript
enum OrderStatus {
  Pending   = "PENDING",
  Paid      = "PAID",
  Shipped   = "SHIPPED",
  Delivered = "DELIVERED",
  Refunded  = "REFUNDED",
  Cancelled = "CANCELLED",
}

type OrderStatusName = keyof typeof OrderStatus;

const ALL_STATUSES: readonly OrderStatus[] = Object.values(OrderStatus);

function transitions(current: OrderStatus): readonly OrderStatus[] {
  switch (current) {
    case OrderStatus.Pending:   return [OrderStatus.Paid, OrderStatus.Cancelled];
    case OrderStatus.Paid:      return [OrderStatus.Shipped, OrderStatus.Refunded];
    case OrderStatus.Shipped:   return [OrderStatus.Delivered, OrderStatus.Refunded];
    case OrderStatus.Delivered: return [OrderStatus.Refunded];
    case OrderStatus.Refunded:
    case OrderStatus.Cancelled: return [];
  }
}

for (const s of ALL_STATUSES) {
  console.log(`${s} -> [${transitions(s).join(", ")}]`);
}

Output:

text
PENDING -> [PAID, CANCELLED]
PAID -> [SHIPPED, REFUNDED]
SHIPPED -> [DELIVERED, REFUNDED]
DELIVERED -> [REFUNDED]
REFUNDED -> []
CANCELLED -> []

Recipe 3 — Bitwise permission flags

A permissions system that packs admin/editor/viewer roles into a single integer for fast intersection checks.

typescript
enum Perm {
  None   = 0,
  View   = 1 << 0, // 1
  Edit   = 1 << 1, // 2
  Delete = 1 << 2, // 4
  Admin  = 1 << 3, // 8
  Viewer = View,
  Editor = View | Edit,
  Owner  = View | Edit | Delete | Admin,
}

function has(perms: Perm, required: Perm): boolean {
  return (perms & required) === required;
}

const user: Perm = Perm.Editor;
console.log(has(user, Perm.View));   // true
console.log(has(user, Perm.Edit));   // true
console.log(has(user, Perm.Delete)); // false
console.log(has(user, Perm.Admin));  // false

Output:

text
true
true
false
false

Recipe 4 — Runtime validation of enum values from JSON

External JSON does not respect enum nominal typing. Validate at the boundary, then pipe the narrowed value to the rest of the code.

typescript
enum Role {
  Admin  = "ADMIN",
  Editor = "EDITOR",
  Viewer = "VIEWER",
}

function isRole(val: unknown): val is Role {
  return typeof val === "string" && (Object.values(Role) as string[]).includes(val);
}

function parseUser(raw: unknown): { id: string; role: Role } {
  if (typeof raw !== "object" || raw === null) throw new Error("not an object");
  const obj = raw as Record<string, unknown>;
  if (typeof obj["id"] !== "string") throw new Error("missing id");
  if (!isRole(obj["role"])) throw new Error(`invalid role: ${String(obj["role"])}`);
  return { id: obj["id"], role: obj["role"] };
}

const incoming = JSON.parse('{"id":"u_1","role":"EDITOR"}');
const user = parseUser(incoming);
console.log(user);

Output:

text
{ id: 'u_1', role: 'EDITOR' }

Recipe 5 — Migrating from enum to as const object

A scriptable refactor that converts a string enum to the modern as const pattern while preserving every call site that uses Status.Active access.

typescript
// Before
enum Status {
  Pending  = "PENDING",
  Active   = "ACTIVE",
  Inactive = "INACTIVE",
}
function setStatus(s: Status) { console.log(s) }
setStatus(Status.Active);

// After — identical call-site syntax, accepts raw strings, tree-shakeable
const Status = {
  Pending:  "PENDING",
  Active:   "ACTIVE",
  Inactive: "INACTIVE",
} as const;
type Status = (typeof Status)[keyof typeof Status];

function setStatus2(s: Status) { console.log(s) }
setStatus2(Status.Active);   // works
setStatus2("ACTIVE");        // also works (new — and matches JSON behavior)

Recipe 6 — Mapping enum to display labels

A common UI pattern: every enum value has a human-friendly label. Pair an enum with a record keyed on the enum to keep the mapping exhaustive.

typescript
enum Priority {
  Low    = "LOW",
  Medium = "MEDIUM",
  High   = "HIGH",
  Urgent = "URGENT",
}

const PRIORITY_LABEL: Record<Priority, string> = {
  [Priority.Low]:    "Low",
  [Priority.Medium]: "Medium",
  [Priority.High]:   "High",
  [Priority.Urgent]: "Urgent — drop everything",
};

function describe(p: Priority): string {
  return `[${p}] ${PRIORITY_LABEL[p]}`;
}

console.log(describe(Priority.Urgent));

Output:

text
[URGENT] Urgent — drop everything

Adding a new enum value here causes a compile error in PRIORITY_LABEL until you provide a label — Record<Priority, string> enforces exhaustive keys.