DEV Community

Cover image for How to extend enums in TypeScript
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

How to extend enums in TypeScript

Written by Kealan Parr✏️

TypeScript is well-loved by the developer community for many reasons, one of which is because of the static checks it provides to the code written in it.

Spotting problems early in your development lifecycle can save days of debugging random, vague errors that can sometimes pop up when using dynamic languages like JavaScript.

TypeScript can help make your code more predictable and better documented, make refactoring easier, and help reduce potential errors you might face at runtime in your production app. Its popularity and power are demonstrated by its 93% developer satisfaction rate and its skyrocketing usage over the past five years.

One language mechanism that is pivotal to TypeScript is enums. In this article, we’ll discuss:

What are enums in TypeScript?

Enums aren’t a feature of typing, interestingly, like most of TypeScript is — in fact, they are one of the few, new features that enhance the language.

Enums allow developers to define a strict set of options for a variable. For example:

enum Door {
  Open,
  Closed,
  Ajar // half open, half closed
}
Enter fullscreen mode Exit fullscreen mode

Enums default to number enums, so the above enum is essentially an object with 0, 1, and 2 as its key, which we can see in the transpiled JavaScript code.

"use strict";
var Door;
(function (Door) {
    Door[Door["Open"] = 0] = "Open";
    Door[Door["Closed"] = 1] = "Closed";
    Door[Door["Ajar"] = 2] = "Ajar"; // half open, half closed
})(Door || (Door = {}));
console.log(Door.FullyOpened);
Enter fullscreen mode Exit fullscreen mode

In TypeScript, you can also use string enums, like so:

enum Door {
  Open = "open",
  Closed = "closed",
  Ajar = "ajar" // half open, half closed
}
Enter fullscreen mode Exit fullscreen mode

If you then used this Door enum, you could ensure that variables only used the three options specified in the enum. So, you couldn’t assign something incorrectly by accident or easily create bugs this way.

If you do try to use another variable, it will throw a type error like this:

enum Door {
  Open = "open",
  Closed = "closed",
  Ajar = "ajar" // half open, half closed
}
console.log(Door.FulyOpened)
Enter fullscreen mode Exit fullscreen mode

Property 'FullyOpened' does not exist on type 'typeof Door'.

Why do we need to extend an enum?

Extension is one of the four pillars of object orientation and is a language feature present in TypeScript. Extending an enum allows you to essentially copy a variable definition and add something extra to it.

So, for example, you might be trying to do something like this:

enum Door {
  Open = "open",
  Closed = "closed",
  Ajar = "ajar" // half open, half closed
}

enum DoorFrame extends Door { // This will not work!
  Missing = "noDoor"
}

console.log(DoorFrame.Missing)
Enter fullscreen mode Exit fullscreen mode

We could then add extra properties into an enum, or even merge two enums together, to still get strict typing on our enum while also being able to change them after they’ve been defined.

But notice that the above code snippet doesn’t work! It fails to transpile and throws four different errors.

Can you extend enums?

The short answer is no, you can’t extend enums because TypeScript offers no language feature to extend them. However, there are workarounds you can utilize to achieve what inheritance would.

Type intersection in TypeScript

enum Door {
  Open = "open",
  Closed = "closed",
  Ajar = "ajar" // half open, half closed
}

enum DoorFrame {
  Missing = "noDoor"
}

type DoorState = Door | DoorFrame; 
Enter fullscreen mode Exit fullscreen mode

In the above code block, we used an intersection type. The intersection acts like an “or,” which simply means that the DoorState type will either be of type Door or of type DoorFrame.

This now means DoorState can use either of the variables from the two enums interchangeably.

Spread syntax

We have seen in the transpiled code earlier that an enum becomes a JavaScript object, with the keys and values that your enum specifies.

In TypeScript, we could write purely JavaScript if we wanted to. In fact, this is one big strength of TypeScript. You could, for example, rename all your file.js to file.ts and turn off the compiler checks for your code. As long as you run the compile/transpile steps, everything would work fine, with zero code changes.

We can make use of this by knowing that when our enum turns into JavaScript, it will be a JavaScript object literal and use the spread syntax, like below:

enum Move {
  LEFT = 'Left',
  RIGHT = 'Right',
  FORWARD = 'Forward',
  BACKWARD = 'Backward'
}
const myMove = {
  ...Move,
  JUMP: 'Jump'
}
Enter fullscreen mode Exit fullscreen mode

This solution has been described secondly, though, as it isn’t as good of a solution as the intersection type because it isn’t as robust as our first solution. This is because the “composition” of your enum is occurring at run time, whereas when we use type intersection, type checking can occur at compile/transpile time, not runtime.

TypeScript enum best practices

We have discussed how we can extend enums in Typescript, but enums aren’t a magic bullet to be used to fix all problems. Enums, when used incorrectly, can make your code readability, scalability, and maintainability worse, rather than improve your code.

So, let’s cover some best practices and common patterns to use when working with enums in TypeScript.

1. Avoid heterogenous enums

I have explained how we can have string enums like this:

enum Seasons {
  Summer = "Summer",
  Winter = "Winter",
  Spring = "Spring",
  Fall = "Fall"
}
Enter fullscreen mode Exit fullscreen mode

Alongside also having numerical enums like this:

enum Decision {
  Yes,
  No
}
Enter fullscreen mode Exit fullscreen mode

But, there is a third type of enum, which you may not be aware of, called a heterogenous enum. This is where you can use a string and numerical enums in the same enum.

An example from the docs is this:

enum BooleanLikeHeterogeneousEnum {
  No = 0,
  Yes = "YES",
}
Enter fullscreen mode Exit fullscreen mode

It’s important to note that even the docs discourage this practice, as in this instance, using this method indicates you likely need to:

  • Reconsider the relationship between these two variables
  • Create two separate enums
  • Make them both conform to one data type

2. The “enums as configuration” anti-pattern

Sometimes code functionality can be forced to adhere to an enum option, which can quickly turn into an antipattern.

Here’s an example:

>enum Operators {
  Add,
  Subtract
}
function calculate(op: Operators, firstNumber: number, secondNumber: number) {
  switch(op) {
    case Operators.Add: return firstNumber + secondNumber
    case Operators.Subtract: return firstNumber - secondNumber
  }
} 
Enter fullscreen mode Exit fullscreen mode

The above code looks fairly simple and safe, because our example is, indeed, simple and safe.

But in large codebases, when you strictly tie implementation details to enum types like this, you can cause a few issues:

  • You create two sources of truth (both the enum and the function need to be updated if the enum changes)
  • This pattern is going to spread metadata around the code
  • The code block is no longer generic

If you need to do something like the above, a simpler (and more condensed) pattern could look like this.

const Operators = {

  Add: {
    id: 0,
    apply(firstNumber: number, secondNumber: number) { return firstNumber + secondNumber }
  },

  Subtract: {
    id: 1,
    apply(firstNumber: number, secondNumber: number) { return firstNumber - secondNumber }
  }
}
Enter fullscreen mode Exit fullscreen mode

You can read more about this pattern here or here.

3. The types of data that enums best represent

There is a way of generally grouping together different types of data utilized in code: discrete variables or continuous variables.

Discrete variables are data that have spaces between their representations, and have only a few representations. Here are a few examples:

  • Days of the week
    • Mon
    • Tue
    • Wed
    • Thur
    • Fri
    • Sat
    • Sun
  • Seasons
    • Summer
    • Winter
    • Spring
    • Fall

Discrete data is a good candidate to be placed inside an enum, and it can help code clarity and reuse. Continuous data refers to data without gaps that fall into a continuous sequence, like numbers. These can be different depending on their measurement:

  • Someone's weight
  • The speed of a car

Discrete data is a good candidate for data, and it can be used in an enum, whereas continuous data should not be used in an enum. Can you imagine an enum for age?

enum Age {
  Zero,
  One,
  Two,
  Three,
  Four,
  Five,
  Six
}
Enter fullscreen mode Exit fullscreen mode

This is not a good candidate to be placed in an enum because it will need to be continuously updated and amended, leading to a maintenance nightmare.

You should only look to add discrete, highly stable types of data inside an enum.

Conclusion

I hope this article has been useful so you better understand what enums are, the problems they solve, the use cases for merging two enums, and how you might achieve it! Happy hacking.


Writing a lot of TypeScript? Watch the recording of our recent TypeScript meetup to learn about writing more readable code.

Write More Readable Code with TypeScript 4.4

TypeScript brings type safety to JavaScript. There can be a tension between type safety and readable code. Watch the recording for a deep dive on some new features of TypeScript 4.4.

Top comments (0)