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.