Skip to main content

How to Convert TypeScript Enums to Strings

You log an enum value during debugging and see 0 instead of "Active". Or you try to display an enum in the UI and it renders as a number. These are the two most common moments when you realize TypeScript's numeric enums aren't self-describing, and you need a reliable way to get human-readable strings from them.

This guide covers the practical patterns for converting TypeScript enum values to strings, including the key differences between numeric and string enums, type-safe approaches, and the mistakes that cause silent bugs.

Converting a TypeScript Enum to a String

For numeric enums, TypeScript generates a reverse mapping at runtime, which lets you look up the name of an enum value using bracket notation:

enum Status {
Active,
Inactive,
Pending
}

const status = Status.Active;
const statusString = Status[status]; // "Active"

console.log(statusString); // Output: "Active"

This works because TypeScript compiles numeric enums into an object with entries in both directions: { Active: 0, 0: "Active", Inactive: 1, 1: "Inactive", ... }. The bracket lookup finds the name for a given value.

This technique is useful for displaying enum values in UI components or debug messages in your Convex apps.

Numeric vs String Enums: How Conversion Differs

One of the most common stumbling blocks is assuming that string enum conversion works the same way as numeric enum conversion. It doesn't.

With a numeric enum, TypeScript creates the reverse mapping automatically:

enum Direction {
North,
South,
East,
West
}

// Reverse mapping works
console.log(Direction[Direction.North]); // "North"
console.log(Direction[0]); // "North"

With a string enum, there is no reverse mapping. Each member has an explicit string value, so TypeScript doesn't generate one:

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

// This does NOT work - returns undefined
console.log(ApiStatus[ApiStatus.Success]); // undefined

// Access the value directly instead
const status = ApiStatus.Success;
console.log(status); // "SUCCESS"

For string enums, the value is the string. You just use it directly. The reverse mapping approach is numeric-only.

Mapping TypeScript Enums to String Representations

When you need custom display strings that differ from the enum's technical names, use a Record type to create a typed lookup:

enum UserRole {
Admin,
User,
Guest
}

const userRoleLabels: Record<UserRole, string> = {
[UserRole.Admin]: 'Administrator',
[UserRole.User]: 'Registered User',
[UserRole.Guest]: 'Site Guest'
};

const role = UserRole.Admin;
console.log(userRoleLabels[role]); // "Administrator"

Using Record<UserRole, string> instead of a generic index signature means TypeScript will error if you add a new enum member and forget to add a matching label entry. That's a useful safety check, especially for i18n work where missing a translation is easy to overlook.

You can integrate this with Convex's type system to keep your backend and frontend string representations in sync across a shared schema.

Type-Safe Enum Strings with keyof typeof

For cases where you need to work with enum names (not values) in a type-safe way, the keyof typeof pattern gives you a string union of the enum's keys:

enum HttpMethod {
GET,
POST,
PUT,
DELETE
}

type HttpMethodName = keyof typeof HttpMethod; // "GET" | "POST" | "PUT" | "DELETE"

function getMethodByName(name: HttpMethodName): HttpMethod {
return HttpMethod[name];
}

// TypeScript will catch invalid names at compile time
getMethodByName("GET"); // fine
getMethodByName("PATCH"); // Error: Argument of type '"PATCH"' is not assignable

This is useful when you're accepting enum names from external input (like form values or URL params) and want compile-time guarantees rather than silent undefined at runtime. The typeof operator article goes deeper on this pattern, and you'll see it used heavily in end-to-end type safety work.

Iterating Over a TypeScript Enum and Getting String Values

To iterate over a TypeScript enum and get string values, use Object.keys() or a for...in loop:

enum Permission {
Read,
Write,
Delete,
Admin
}

// Using Object.keys()
Object.keys(Permission).forEach((key) => {
if (isNaN(Number(key))) {
console.log(key); // "Read", "Write", "Delete", "Admin"
}
});

// Using for...in loop
for (const key in Permission) {
if (isNaN(Number(key))) {
console.log(key); // "Read", "Write", "Delete", "Admin"
}
}

The isNaN(Number(key)) check is crucial because TypeScript creates a bidirectional mapping for numeric enums, meaning both names and values are accessible as keys. Without it, you'd also get "0", "1", "2", "3" mixed in with the names.

It's the go-to approach when you need to populate a UI element with all possible enum options: a permissions picker, a filter dropdown, a settings form. You can combine it with Array methods to transform enum values into the exact shape your component expects.

Using TypeScript Enums as String Keys in Object Lookups

When you need to use enum string names as keys in objects, combine the bracket notation with object literal syntax:

enum Environment {
Development,
Staging,
Production
}

const envDescriptions: { [key: string]: string } = {
[Environment[Environment.Development]]: 'Local dev server',
[Environment[Environment.Staging]]: 'Pre-release testing',
[Environment[Environment.Production]]: 'Live traffic'
};

const env = Environment.Production;
const envName = Environment[env]; // "Production"
const description = envDescriptions[envName]; // "Live traffic"

console.log(description); // Output: "Live traffic"

The enum name becomes the key, and any data you need lives in the value. It works well with the switch statement alternative when you need more complex logic based on enum values.

Where Things Go Wrong

A few mistakes come up repeatedly when developers convert enums to strings:

Using reverse mapping on string enums. As covered above, EnumName[EnumName.Value] returns undefined for string enums. TypeScript won't warn you, so the bug shows up silently at runtime. If you're working with string enums, use the value directly.

Object.keys() returning numeric index keys. With numeric enums, Object.keys(Status) returns ["0", "1", "2", "Active", "Inactive", "Pending"] because of the bidirectional mapping. Always filter with isNaN(Number(key)) when you want only the names.

Passing a string value where a name is expected. With string enums like ApiStatus.Success = "SUCCESS", the value "SUCCESS" is not the same as the key "Success". If you pass "SUCCESS" to a function expecting a keyof typeof ApiStatus, it won't match.

Assuming const enums behave the same. const enum members are inlined at compile time and don't exist as objects at runtime, so Object.keys(), Object.values(), and reverse mapping all fail. If you need runtime access to enum names, use a regular enum.

Consider using as const objects as an alternative when you need full runtime access to keys and values without these edge cases.

Outputting TypeScript Enum Values as Strings for Debugging

When your logs print 2 and you can't tell which enum member that is, a quick template literal conversion clears it up:

enum LogLevel {
Debug,
Info,
Warning,
Error
}

const level = LogLevel.Warning;
const levelName = LogLevel[level]; // "Warning"

console.log(`[${levelName}] Enum value is ${level}`); // "[Warning] Enum value is 2"

It's a small habit that pays off whenever you're working with numeric enums in structured logging. See the keyof operator article for how to take this further with type-level introspection.

Creating a Function to Return String Names from Enum Values

If you need the same conversion in multiple places, a helper function is cleaner than repeating the bracket lookup:

enum OrderStatus {
Pending,
Processing,
Shipped,
Delivered,
Cancelled
}

function getOrderStatusLabel(status: OrderStatus): string {
return OrderStatus[status];
}

console.log(getOrderStatusLabel(OrderStatus.Shipped)); // "Shipped"

In production, add a fallback for unexpected values:

function getOrderStatusLabel(status: OrderStatus): string {
const label = OrderStatus[status];
return label ?? 'Unknown';
}

The nullish coalescing fallback prevents your UI from rendering undefined if an unexpected value slips through. For Convex applications, pairing this with a shared schema type ensures the same labels render consistently on both the server and client.

Final Thoughts

Enum-to-string conversion is really two separate problems. Numeric enums need a reverse mapping lookup. String enums don't, because the value is already a string.

The patterns in this article cover most real-world cases. Bracket notation for quick lookups. Record<Enum, string> for exhaustive label maps that TypeScript keeps honest. keyof typeof when you need enum names as a type-safe string union. And the isNaN filter whenever you iterate over a numeric enum's keys.

The "Where Things Go Wrong" section covers the mistakes that cause silent bugs. Worth skimming before you ship enum conversion code in production.

When building full-stack TypeScript apps, keeping these conversions in typed utility functions ensures consistent display across your frontend and backend rather than re-deriving them inline each time.