Skip to main content

TypeScript Enums for Defining Named Constants

TypeScript enums provide a powerful way to define a set of named constants, making your code more maintainable, type-safe, and self-documenting.

Think of them as a way to group related values—like roles, states, or options—under one neat umbrella. Instead of scattering random strings or numbers throughout your code, enums let you define a set of constants with meaningful names.

Here's a simple example:

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

This enum defines user permission levels in an application. Instead of using raw strings like "ADMIN" or "EDITOR" throughout the code, it allows you to use UserRole.Admin or UserRole.Editor. This prevents typos, enables autocomplete, and makes it easier to track and update role names across your codebase. We'll revisit this example in use cases.

In this article we'll cover:

  • Ambient vs non-ambient enums
  • Enum mapping (bidirectional and unidirectional)
  • Constant and computed enum member values
  • How enums translate to JS
  • Heterogeneous enums
  • Numeric enums
  • Enum use cases

Ambient Enums in TypeScript

Ambient enums are a special type of enum used when the enum values are defined externally, often in a separate library or environment, and need to be declared in TypeScript to enable type-checking.

Ambient vs. Non-Ambient Enums

  • Ambient Enums: Declared with the declare keyword and exist only at compile time. They don't generate any JavaScript output.
  • Non-Ambient Enums: Declared normally and are fully compiled into JavaScript.

Here's an example:

// Ambient Enum
declare enum ExternalEnum {
Start = 1,
Stop = 2,
}

// Non-Ambient Enum
enum InternalEnum {
Start = 1,
Stop = 2,
}

Limitations of Ambient Const Enums

Ambient enums cannot include computed or non-literal values:

declare const enum ExternalEnum {
Value = Math.random(), // Error: Ambient const enums can only have literal values
}

Enum Mapping in TypeScript

TypeScript supports both unidirectional and bidirectional mappings for enums, which allow you to look up values either by name or by value.

Unidirectional Mapping

In a string enum, you can map from the key to its value:

enum StringEnum {
Pending = "PENDING",
Approved = "APPROVED",
}

console.log(StringEnum.Pending); // Output: "PENDING"

Bidirectional Mapping

For numeric enums, TypeScript automatically creates both forward and reverse mappings:

enum NumericEnum {
Start = 1,
Stop,
}

console.log(NumericEnum.Start); // Output: 1
console.log(NumericEnum[1]); // Output: "Start"

This bidirectional behavior is unique to numeric enums and doesn't apply to string enums.

Constant and Computed Enum Member Values

Enum members can be either constant or computed. Constant members are known at compile-time, while computed members are evaluated at runtime.

Constant Members

Constant members can be explicitly or implicitly initialized:

enum Constants {
A = 1, // Explicit
B, // Implicit (auto-increment from A)
C = 10, // Explicit
}
console.log(Constants.B); // Output: 2

Computed Members

Computed members require runtime evaluation:

enum Computed {
A = 10,
B = A * 2, // Computed
}
console.log(Computed.B); // Output: 20

String Enum Initialization

String enum members must always be explicitly initialized:

enum StringEnum {
A = "Alpha",
B = "Beta",
}

How Enums Translate to JavaScript

At runtime, enums translate into plain JavaScript objects. Here's what happens:

Numeric Enums

For a numeric enum:

enum Role {
Admin = 1,
Editor,
}

console.log(Role.Admin); // Output: 1

It translates to:

var Role;
(function (Role) {
Role[(Role["Admin"] = 1)] = "Admin";
Role[(Role["Editor"] = 2)] = "Editor";
})(Role || (Role = {}));

Note the reverse mapping created for numeric enums.

String Enums

For string enums, only forward mappings are generated:

enum Status {
Pending = "PENDING",
Approved = "APPROVED",
}

Translates to:

var Status = {
Pending: "PENDING",
Approved: "APPROVED",
};

Heterogeneous Enums

Heterogeneous enums mix numeric and string values in the same enum. While less common, they provide flexibility:

enum Heterogeneous {
No = 0,
Yes = "YES",
}
console.log(Heterogeneous.No); // Output: 0
console.log(Heterogeneous.Yes); // Output: "YES"

When to Use: Heterogeneous enums are useful in cases where you need flexibility, but they can make code harder to maintain.

Numeric Enums

Numeric enums are the most commonly used enums in TypeScript. By default, numeric enums auto-increment:

enum AutoIncrement {
A = 1,
B,
C,
}
console.log(AutoIncrement.B); // Output: 2

Fully Initialized Numeric Enums

You can explicitly assign values to all members:

enum FullyInitialized {
A = 10,
B = 20,
C = 30,
}
console.log(FullyInitialized.B); // Output: 20

Reverse Mapping

Numeric enums support reverse mapping, allowing you to look up a key by its value:

enum ReverseMapping {
A = 1,
B = 2,
}

console.log(ReverseMapping[1]); // Output: "A"

3 Real-World Examples of Enums

Let's talk about some situations where enums make your life easier:

1. Handling User Roles

Managing user permissions can get messy fast. Enums make it super clear who can do what.

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

function canEditContent(role: UserRole): boolean {
return role === UserRole.Admin || role === UserRole.Editor;
}

// Example usage:
console.log(canEditContent(UserRole.Admin)); // Output: true
console.log(canEditContent(UserRole.Viewer)); // Output: false

No more wondering if "admin" is supposed to be uppercase, lowercase, or something else. It's always UserRole.Admin.

2. Standardizing API Statuses

If your app talks to an API (and let's face it, most do), enums are fantastic for making sure your responses are consistent.

enum ApiResponseStatus {
Success = "SUCCESS",
Error = "ERROR",
Loading = "LOADING",
}

function displayApiResponse(status: ApiResponseStatus): void {
switch (status) {
case ApiResponseStatus.Success:
console.log("Data fetched successfully!");
break;
case ApiResponseStatus.Error:
console.log("There was an error fetching the data.");
break;
case ApiResponseStatus.Loading:
console.log("Data is still loading...");
break;
}
}

With this setup, you're not just winging it with random strings like "success". You have a clear, standard way to handle every response.

3. Switching Themes in Your App

Want to add a dark mode or high-contrast theme? Enums make it easy to manage all your app's themes in one place.

enum AppTheme {
Light = "light",
Dark = "dark",
HighContrast = "high-contrast",
}

function applyTheme(theme: AppTheme): void {
console.log(`Applying ${theme} theme...`);
document.body.className = theme;
}

// Example usage:
applyTheme(AppTheme.Dark); // Output: Applies the dark theme

Switching themes becomes as simple as calling applyTheme(AppTheme.Dark). No more accidental typos like "darcc".

How Enums Work with Convex

Convex schemas don't support enums directly, but you can simulate their use with Convex by validating the values in your mutation functions using a union of literals:

enum OrderStatus {
Pending = "PENDING",
Shipped = "SHIPPED",
Delivered = "DELIVERED",
}

const orderStatusValidator = v.union(
v.literal(OrderStatus.Pending),
v.literal(OrderStatus.Shipped),
v.literal(OrderStatus.Delivered)
);

// Or more concisely
//
// const orderStatusValidator = v.union(
// ...Object.values(OrderStatus).map(v.literal)
// );

// Define schema
const orderSchema = defineTable({
status: orderStatusValidator,
orderId: v.string(),
// other fields...
});

// Use enum in mutation
export const updateOrderStatus = mutation({
args: {
orderId: v.id("orders"),
status: orderStatusValidator,
},
handler: async (ctx, args) => {
await ctx.db.patch(args.orderId, {
status: args.status,
});
},
});

This ensures consistent and type-safe values for database fields or API responses.

Keep Your Frontend and Backend in Sync with TypeScript Enums

Enums are one of those TypeScript features that you don't fully appreciate until you've tried them. They simplify your code, make it more reliable, and keep everything consistent—whether you're working on a small project or a massive app powered by Convex.

With Convex's TypeScript-first approach, you can define enums or simulate them with discriminated unions to ensure your backend is robust and consistent. So, next time you're tempted to type "admin" or "status" for the 10th time, think about using an enum instead.