Skip to content

Typescript Enums: A Word of Caution

Posted on:April 28, 2023 at 10:15 AM

TypeScript enums may seem like a convenient way to define related constants, but they come with several drawbacks you should be aware of. If you’re considering using enums in your project, it’s worth taking a moment to reconsider due to the following reasons.

Table of contents

Open Table of contents

Reason 1: JavaScript has no native Enum

TypeScript is essentially JavaScript with added syntax for types. To use TypeScript effectively, it is important to view it as a type-only addition to JavaScript. However, the Enum type breaks this rule.

To illustrate this point, let’s consider a few examples of the data types available in JavaScript.

// ./main.js

const strVar = "Robin";
connsole.log(typeof strVar); // --> string

const numVar = 523;
connsole.log(typeof numVar); // --> number

const bigIntVar = BigInt(523);
connsole.log(typeof bigIntVar); // --> "bigint"

const boolVar = true;
connsole.log(typeof boolVar); // --> boolean

const undeVar = undefined;
connsole.log(typeof undeVar); // --> undefined;

const nullVar = null;
connsole.log(typeof nullVar); // --> object;

const objectVar = { name: "Robin", age: 23 };
connsole.log(typeof objectVar); // --> object

const arrVar = ["david", "roy", "tim"];
connsole.log(typeof arrVar); // --> object

In Typescript, there is an equivalent type for every datatype available in Javascript. These include number, string, bigint, boolean, undefined, and an interface for object types, among others. However, there is no direct mapping between a specific datatype in Javascript and the enum type in Typescript.

💡 Typescript Handbook : Unlike most TypeScript features, this is not a type-level addition to JavaScript but something added to the language and runtime. Because of this, it’s a feature which you should know exists, but maybe hold off on using unless you are sure.

Several proposals have been made to include the Enum type in ECMAScript (Javascript), such as those found in the repositories of rbuckton and Jack-Works on GitHub. However, despite these efforts, the proposal has not gained significant traction in the development community.

Numeric Enum:

Reason 2: Common Developer Mistakes

Developers can easily overlook how the Enum type works, potentially causing serious problems for their application. Consider the following example:

enum BlogCategory {
  food,
  travel,
}
console.log(BlogCategory.food); // 0
console.log(BlogCategory.travel); // 1

Now suppose we need to add a new category, “tech.” A developer might make the following mistake:

enum BlogCategory {
  tech, // <------- this is mistake
  food,
  travel,
}
console.log(BlogCategory.food); // 1
console.log(BlogCategory.travel); // 2

As you can see, the values for BlogCategory.food and BlogCategory.travel are no longer 0 and 1, respectively. And if the Enum is used in other parts of the application, such as when sending data over a network or storing it in a database then congratulations - you may have just broke the entire app!

Fortunately, there’s an easy fix for the issue, we could initialize the Enum with fixed values

enum BlogCategory {
  tech = 2,
  food = 0,
  travel = 1,
}

console.log(BlogCategory.food); // 0 <-- this is correct again
console.log(BlogCategory.travel); // 1 <-- this is correct again

As you can see, the values for food and travel are now back to their original values of 0 and 1, respectively.

Reason 3: Enum limitation and TS compiler - 1

Compiler is often referred to as a developer’s best friend because it helps to avoid common errors and ensures that code runs as expected if it compiles successfully. In fact, working with the compiler can feel like you’re programming in pairs with a trusted teammate.

However, when it comes to Enums, TypeScript’s support is limited and We have to rely solely human intervention when initializing Enums

enum BlogCategory {
  tech = 2,
  food = 0,
  travel = 1,
  sport = 2, // <---- Compiler will not throw error 🤔
}

Here, we have two Enum values (tech and sport) that share the same numeric value of 2. This is clearly a mistake that should be caught by the compiler, but TypeScript does not provide any warnings or errors in such cases. As a result, it’s up to the developer to be diligent and ensure that Enums are initialized correctly.

Reason 4: Enum limitation and TS compiler - 2

TypeScript enums can pose a problem when used as function parameters or return types, as the compiler may rely on the developer’s due diligence in these cases.

enum BlogCategory {
  food = 0,
  travel = 1,
  tech = 2,
}

function filterBlogsby_V1(category: BlogCategory) {}

filterBlogsby_V1(BlogCategory.food);
filterBlogsby_V1(BlogCategory.travel);
// Of course, this should work as expected

filterBlogsby_V1(0);
filterBlogsby_V1(1);
// with no suprice this should also work as expected

// case:1
filterBlogsby_V1(100); // 🤦🏻‍♂️ why is typescript allowing random number as function input

// case:2
function getblogType_v1(blogs: any[]): BlogCategory {
  // return BlogCategory.food;
  // return BlogCategory.travel;
  return BlogCategory.tech;
}
// all the return value will work

function getblogType_v2(blogs: any[]): BlogCategory {
  // return 0;
  // return 1;
  return 2;
}
// all the return number should work here

function getblogType_v3(blogs: any[]): BlogCategory {
  return 60; // 🤦🏻‍♂️ , this should have been error but its not.
}

As shown in the examples above, TypeScript may allow unexpected values to be used with enums, such as random numbers passed as function input.

Additionally, the return values of functions using enums are not type-checked, which could lead to unexpected behavior if an invalid value is returned.

NOTE: While this issue has been fixed in TypeScript 5.0.4, it is still important to be aware of the potential risks when using enums in TypeScript.

Reason 5: Reverse mappings and lookup

Numeric enums in TypeScript not only create an object with property names for members, but they also get a reverse mapping from enum values to enum names. While it might seem like a nice feature, it can actually be confusing and easily introduce issues into our codebase.

Consider the following example:

enum BlogCategory {
  food = 0,
  travel = 1,
  tech = 2,
}

console.log(BlogCategory["travel"]); // 1
console.log(BlogCategory["tech"]); // 2

// reverese lookup

console.log(BlogCategory[1]); // 'travel'
console.log(BlogCategory[2]); // 'food'
console.log(BlogCategory[100]); // undefined

To support both types of lookup, TypeScript needs to create an object that has both the string and number as keys. In the compiled JavaScript, the BlogCategory object looks like this:

var BlogCategory;
(function (BlogCategory) {
  BlogCategory[(BlogCategory["food"] = 0)] = "food";
  BlogCategory[(BlogCategory["travel"] = 1)] = "travel";
  BlogCategory[(BlogCategory["tech"] = 2)] = "tech";
})(BlogCategory || (BlogCategory = {}));

console.log(BlogCategory);
// {0: 'food', 1: 'travel', 2: 'tech', food: 0, travel: 1, tech: 2}

Because we have both string and number keys in the enum object, **Object.keys**can also create issues.

enum BlogCategory {
  food = 0,
  travel = 1,
  tech = 2,
}

console.log(Object.keys(BlogCategory));
// this is what you expected:  ['food', 'travel', 'tech']
// this is what you got:  ['0', '1', '2', 'food', 'travel', 'tech']

As you can see, Object.keys is returning all the enum values as string keys, along with the string keys we defined during the reverse mapping. This can cause issues when iterating over an enum with for...in loop or using Object.keys.

Let’s summarize the issues we have encountered with Numeric Enum:

  1. Unstable over networks when sending data to the server or storing in the database.
  2. Reverse mapping and lookup pose a challenge.
  3. No type safety when used as function input type or return type.

String enum

String enums offer some improvements over numeric enums, including the ability to solve issues related to unstable network data, reverse mapping, and lack of type safety.

// solves (1) st issue: Unstable over networks when sending data to the server or storing in the database.
enum BlogCategory {
  //   tech = "tech", < -- Adding this later will not create issue
  food = "food",
  travel = "travel",
}

// solves (2) nd issue: Reverse mapping and lookup pose a challenge.
console.log(Object.keys(BlogCategory)); // ✅ ['food', 'travel'] -  expected output
console.log(BlogCategory["food"]); //  ✅ "food" - no reverse lookup allowed.

// solves (3) rd issue: No type safety when used as function input type or return type.
function filterBlog(type: BlogCategory) {}
filterBlog(BlogCategory.food); // ✅ <--  allowed
filterBlog(0); // ❌ <--  not allowed
//        ^^^
filterBlog(65); // ❌ <--  not allowed
//        ^^^

Good, Now that we have established that string enums are a slightly better option than numeric enums, as my personal choice i don’t like them either, let me explain

Reason 6: Repetitive and verbose

// enum.ts

enum BlogCategory {
  tech = "tech",
  food = "food",
  travel = "travel",
}

At first glance, there might not appear to be anything technically wrong with this implementation. However, upon closer inspection, it does seem to violate the DRY (Don’t Repeat Yourself) principle and looks quite repetitive and verbose. What are your thoughts?

oh, I sense some disagreement. Well, let’s continue with our example

// utils.ts
import { BlogCategory } from "./enum";

export const filterBlogsBy = (type: BlogCategory) => {};
// main.ts
import { filterBlogsBy } from "./utils";

const filteredblogs = filterBlogsBy("tech"); // ❌ <-- can't call it with valid value
//                                  ^^^^^^
// Argument of type '"tech"' is not assignable to parameter of type 'BlogCategory'.
// main.ts
import { filterBlogsBy } from "./utils";
import { BlogCategory } from "./enum"; // <-- extra import to call a function

const filteredblogs = filterBlogsBy(BlogCategory.tech); // ✅ this works now

While this code works now, take a look at the extra import we have to add just to call the function. Personally, I find it too verbose. It may not seem like a big deal initially, but as your application grows, this additional import can become a challenge when writing new code or participating in large-scale refactoring.

Solution 1: Typescript’s Type Union

What is the alternative to TypeScript enums, then?

One approach is to use TypeScript’s type union feature, which combines the benefits of compile-time type checking with JavaScript’s runtime flexibility.

Here’s an example of how you might define and use a type union in your code:

// appTypes.ts
export type BlogCategory = "tech" | "food" | "travel";
// utils.ts
const filterBlogsBy = (type: BlogCategory) => {};
// main.ts
import { filterBlogsBy } from "./util";
// import { BlogCategory } from './appTypes'; <-- no extra import required.

const filteredblogs = filterBlogsBy("tech"); // ✅ <-- passing string 'tech' works.

const filteredblogs_v1 = filterBlogsBy("tec"); // <-- this is expected
//                                      ^^^^ ❌
// Error: Argument of type '"tec"' is not assignable to parameter of type 'BlogCategory'.

From the above code, it is evident that using TypeScript’s type union as an alternative to enums can provide several benefits, such as:

Solution 2: Typescript’s Type Union and Object with as Const

In some cases, we need to list valid values and iterate over them. While we can use TypeScript enums for this, an alternative approach is to create an object with the valid values and use the keyofoperator to extract them into a type.

const BlogCategory = {
  tech: "tech",
  food: "food",
  travel: "travel",
} as const;

type BlogCategory = keyof typeof BlogCategory;
// similar to
// type BlogCategory = 'tech' | 'food' | 'travel';

const filterBlogsBy = (type: BlogCategory) => {};
// Using 'BlogCategory' as type here. i.e 'type BlogCategory'

const filteredblogs = filterBlogsBy("tech"); // ✅ <-- passing string 'tech' works

const filteredblogs_v1 = filterBlogsBy("food");

console.log(Object.keys(BlogCategory)); // ['tech','food','travel']
// Using 'BlogCategory' as value here. i.e 'const filterBlogsBy'

🤌🏻 nice and clean.

💡 Typescript handbook : The biggest argument in favour of this format over TypeScript’s enum is that it keeps your codebase aligned with the state of JavaScript, and when/if enums are added to JavaScript then you can move to the additional syntax.

Final Thoughts:

Considering all the reason we provided against using enum it is safe to say that with following option in hand we may never need enum

References:

Discuss on