VOOZH about

URL: https://blog.logrocket.com/extend-enums-typescript/

⇱ How to extend enums in TypeScript - LogRocket Blog


2024-08-15
1785
#typescript
Kealan Parr
121310
👁 Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

Editor’s note: This article was last updated by Ibiyemi Adewakun on 15 August 2024 to cover using TypeScript’s const assertion, as const, to extend enums as an alternative to using the union type.

👁 How To Extend Enums In TypeScript

TypeScript is well-loved by the developer community for many reasons, one of which being the static checks it provides to 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.

One language mechanism that is pivotal to TypeScript is enums. In this article, we’ll discuss what enums are, why you would need to extend them and how to do so, and best practices for working with TypeScript enums.

🚀 Sign up for The Replay newsletter

The Replay is a weekly newsletter for dev and engineering leaders.

Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

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. Here’s an example:

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

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 following 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);

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

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

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)
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.

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)

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

But, notice how 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 use to achieve what inheritance would.

The union type in TypeScript

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

enum DoorFrame {
 Missing = "noDoor"
}

type DoorState = Door | DoorFrame; 

let door: DoorState;
door = Door.Ajar
console.log(door) // 'ajar'
door = DoorFrame.Missing
console.log(door) // 'noDoor'

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

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


Over 200k developers use LogRocket to create better digital experiences

👁 Image
Learn more →

However, an important caveat is that we lose one of the biggest benefits of an enum, which is referencing the enum’s options like a normal object property e.g., DoorState.Open or DoorState.Missing.

In TypeScript, it also isn’t possible to use the values of the enums, like ajar and noDoor. Our only option is to reference the individual enums we unioned like DoorFrame.Missing or Door.Open, which is a limitation.

Spread syntax

When our TypeScript enum is transpiled, it becomes a JavaScript object with the keys and values that our enum specifies.

In TypeScript, we could write purely JavaScript if we wanted to. In fact, this is a 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 will work fine, with zero code changes.

So with the understanding that when our enum becomes an object literal when transpiled, we can treat it like a JavaScript object and use the spread syntax like below to create a new object with more properties (options):

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

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

Using as const

Above, we explored using TypeScript’s union type as a way to extend our enums and the limitations of that approach.

Another option is to use TypeScript’s const assertion, as const. To properly understand this option, there are two key things to keep in mind:

  • Enums, when transpiled to JavaScript, are essentially objects
  • TypeScript’s const assertion, when used in the definition of an object, creates the object with read-only properties

Using these properties of const assertions, we can create and combine read-only object literals to our enum options and create a type with this newly formed object. Here’s an example:

const Door = {
 Open: "open",
 Closed: "closed",
 Ajar: "ajar" // half open, half closed
} as const

const DoorFrame = {
 Missing: "noDoor"
} as const

const DoorState = {
 ...Door,
 ...DoorFrame
} as const

type DoorState = typeof DoorState[keyof typeof DoorState]

let door: DoorState;
door = DoorState.Open
console.log("Door has a value matching enum object Door", door)

door = DoorState.Missing
console.log("Door has a value matching enum object DoorFrame", door)

Our above option gives us the ability to get the benefits of an enum using JavaScript object literals.

We can also use the power of TypeScript generics to create a type DoorState that will only allow assignment from the object literal DoorState‘s properties:

door = "apple" // throws Error - Type '"apple"' is not assignable to type 'DoorState'.

door = DoorState["apple"] // sets undefined but throws no errors

If you’re interested, you can learn more about TypeScript’s const assertions here.

TypeScript enum best practices

We have discussed how we can extend enums in Typescript, but enums aren’t a magic trick to be used to fix all problems. When used incorrectly, enums 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"
}

alongside numerical enums like this:

enum Decision {
 Yes,
 No
}

But there is a third type of enum that 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. Here is an example from the docs:

enum BooleanLikeHeterogeneousEnum {
 No = 0,
 Yes = "YES",
}

It’s important to note that while this is a possibility, TypeScript’s documentation discourages this practice. Rather than creating a heterogenous enum such as our above example, you are encouraged 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 anti-pattern.

Here’s an example from the following article:

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
 }
} 

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 }
 }
}

You can read more about this pattern 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 clear spaces between their representations and have only a few representations. Examples include:

  • 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 clear gaps or options; they often have values that can be a continuous sequence, such as numbers. A good example of continuous data is measurements, such as a person’s weight or the speed of a car, because they can have a range of values.

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
}

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

TypeScript enums offer a powerful way to define and manage sets of related values, though they come with limitations. In this article, we explored techniques to work around these limitations by extending enums using union types, as const assertions, and more. By applying these methods, you can maintain strong typing while extending the functionality of enums.

Do you have ideas for extending enums that we didn’t cover in this article? Leave your solutions in the comment section below!

Happy hacking!

LogRocket understands everything users do in your web and mobile apps.

👁 LogRocket Dashboard Free Trial Banner

LogRocket lets you replay user sessions, eliminating guesswork by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks, and with plugins to log additional context from Redux, Vuex, and @ngrx/store.

With Galileo AI, you can instantly identify and explain user struggles with automated monitoring of your entire product experience.

Modernize how you understand your web and mobile apps — start monitoring for free.

👁 Image
👁 Image
👁 Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

What is TSRX?: What JSX would look like if it were designed today

TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension — no new framework required.

👁 Image
Ikeh Akinyemi
Jun 12, 2026 ⋅ 6 min read

How to add authentication to a React Native app with Better Auth

Learn how to build a full React Native auth system using Better Auth and Expo — with email/password login, Google OAuth, session persistence, and protected routes.

👁 Image
Chinwike Maduabuchi
Jun 9, 2026 ⋅ 13 min read

AI dev tool power rankings & comparison [June 2026]

Compare the top AI development tools and models of June 2026. View updated rankings, feature breakdowns, and find the best fit for you.

👁 Image
Chizaram Ken
Jun 8, 2026 ⋅ 11 min read

How to check username availability at scale with Bloom filters

Learn how Bloom filters reduce database lookups for username availability checks while preserving correctness at scale.

👁 Image
Rosario De Chiara
Jun 8, 2026 ⋅ 6 min read
View all posts

Hey there, want to help make our blog better?

Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.

Sign up now