Recreating advanced Enum types in Typescript

Nitzan Hen
The Startup
Published in
9 min readApr 25, 2020

--

Coming into the web development scene from a pure object-oriented programming perspective, over the past year I’ve had to find alternatives to many features and patterns cardinal to OOP programming, but lacking a good implementation in Javascript — e.g. private/protected accessors, or abstract classes. Discovering Typescript helped with many of those, but since Typescript is built on top of (and eventually compiles to) Javascript, it cannot compare to a “true” OOP environment.

While I’ve come to know Javascript’s patterns better over time, and grow more fond of it, one important entity that remained problematic was high-level enums; in languages like Java, or the more progressive Kotlin, enum types have advanced implementations, which allow the programmer to add convenient features — such as custom properties and methods, instance or static.

In this article I’ll be writing about my research on enums and enum patterns in Typescript, and my experience with recreating them. In a followup article, I’ll be doing the same with enums in Javascript — researching which led me down an adventure to the language’s deeper APIs an mechanics.

But what exactly are enums?

In essence, enums (or enumeration types, to be more descriptive) can be thought of like a class, all the instances of which are predetermined and declared statically. They are useful for cases where you have a certain type, which might otherwise be implemented using an object, a TS interface or a class — but you know exactly what its instances are going to be at compile time, before even building the project. In such cases, enums provide enforcement — you give yourself the assurance that another programmer cannot accidentally assign invalid values to those fields — and improve overall readability.

Some example where it may be useful are:

  • Modeling options of a property which may only have one of a few select properties: for example, the CSS ‘position’ property may only be one of five known options (static, relative, fixed, absolute or sticky). If we wanted to make sure the programmer cannot programmatically change the position property of an element, for example, to “Javascript”, or to any value other than the five valid values, we could define an enum, and call it Position, for example.
    In fact, the enum type could be helpful for many CSS properties — such as display, text-align, text-decoration, direction and many more. Many errors could be prevented, even before compiling, by defining enum types and forcing the programmer to set the field to one of the values provided by them; it also improves the brevity of the code.
    Generalizing this example, enums can be quite helpful when creating APIs or libraries purposed for other programmers — they prevent them (and you, too) from passing invalid values to fields that have only a few possible options.
  • Static themes of an app: say you’re building an app which has a light theme and a dark theme, as well as an older “classic” theme. Each theme has its own set of colors (primary, secondary, text colors, etc.) as well as, perhaps, a short description of the theme displayed on the theme selection section, and maybe some other properties. However, these themes, their color palette and their description included, are static — the colors don’t change at runtime (in this particular app; apps with dynamic themes are a different case).
    In this case, it would be wise to use an enum — like a normal Theme class or interface, it encapsulates the data of each theme instance under a unified pattern (the class or interface), which — if exposed correctly — all components in the app can access, but does not allow the creation of new Theme objects at runtime, nor change the existing instances’ properties (by default; properties can optionally be made mutable, as will be explained later on).
  • Logic/data concerns: consider an app dealing with planets in the solar system. Those will have known properties for planet mass, radius, average distance from the sun, etc. We might also want methods to determine the fraction of an orbit of a given planet around the sun, or the minimum distance between a given planet to another planet. Our planets, of course, are a static collection — no planets are added or removed at runtime, and their property values don’t change. Therefore, using an enum is ideal for them — named Planets, for example.

Using the examples above, we can form a list of features that a sufficient enum implementation must have:

  1. The enum’s creator must be able to declare all of the enum’s instances statically, as part of the type itself. This is a trivial one — since we know all of the enum’s options, e.g. which valid values the CSS Position property has, we would want to have them as a constant (and accessible) part of our enum type. If you know exactly what a class’s instances are going to be when writing it — there’s a good chance you need an enum.
  2. An enum must be its own type — you cannot pass an enum instance as an argument for a primitive parameter, or vice versa (unless transforming the value). Also, all instances of the enum belong to the Enum type — “Enum.INSTANCE instanceof Enum” must evaluate to true. This separates the enum options from strings or integers, for example.
  3. After an enum type is created, no more instances of it can be created — another programmer cannot add an additional planet, for example. Technically speaking, this is perhaps the core difference between enums and standard classes (if we choose to see enums as a special type of classes).
  4. An enum instance can have properties and methods — and the enum type may have additional static properties and methods, depending on its definition. This accounts for the additional data and functionality we store for each theme or each planet, in the examples above.
  5. Iterating over all instances of an enum should be simple — preferably, through a static field of the enum type, such as Enum.values, that holds all of the enum’s instances.

Additional, convenient features that an enum implementation should have:

  1. (#6) Enum types should be easily serialized and deserialized — using methods of the enum type (static or instance), we should be able to easily convert an enum instance to a string or number, unique to it, so it can be persisted in a database or loaded to the body of an HTTP request.
  2. (#7) Optionally, enum instances may be made mutable — if the enum’s creator explicitly defines it so. This isn’t recommended for typical enums, but might be preferred over other alternatives. Note that in this case, the instances themselves remain constant (new instances can definitely not be added, or existing ones deleted) — but the instances’ properties’ values themselves can be changed.

If you’re not familiar with Typescript, or need a refresher, check out their docs. From here onward, I’ll be assuming you have basic knowledge of Typescript, and OOP in general:

Recreating enums in Typescript

First of all, we have an important point to address:

Typescript has enums

If you are familiar with Typescript, up until now you may have been internally screaming “Typescript already has enums!”. You are not mistaken — Typescript does have an official implementation of enums, described here.

However, a quick comparison shows that it is not nearly as advanced as developers might need it to be — it’s not much more than a wrapper for strings or numbers. Specifically, criteria #4, #5 are not met, #2 is met partially, and #7 is not really relevant (with TS enums being a string/number wrapper). The existing implementation is also quite messy (const enums, ambient enums, “keyof typeof”, etc.).

Well, a better solution to the enum problem exists, and while it may not be an official part of Typescript, it’s not very complicated. It involves creating a class and configuring it to our needs — what I like to call the Pseudo-enum pattern:

Pseudo-enums

As stated above, the pseudo-enum pattern makes use of simple classes: we declare a class, define what is hidden (private) and what’s accessible from outside (public) — notably, we hide the constructor — and define instances, as well as a .values getter (for all of them) as static properties of that class. Using the Typescript readonly modifier, we can control what can be changed from outside the class (usually, we’d disable any mutability). Of course, we are free to add any instance property or methods just like we would in a normal class.

This pattern is also useful in other OOP environments where enums are lacking in features (I’ve used it in dart, for example, when developing in Flutter).

Let us implement this in an example:

Example — Todo statuses

say we have a simple Typescript todo list app, and we’re currently developing the status field of the todo — for the sake of this example, let’s say each todo can only have one of the statuses of “not started”, “in progress”, “completed”, “cancelled”, or “needs update” (if it’s been completed but needs fixing, for example). We’d eventually also like to include sorting and filtering by status, so we need a way to make sure all todos fall exactly into one of those categories.

In this case, it’s ideal to use an enum — each instance will have a name and an index (by which we can implement, for example, simple sorting). We’ll also add a third field, a description field, which shows up when hovering over the todo’s status, wherever it is displayed. There are also going to be a .values getter and methods to easily serialize and deserialize the instances.

We’ll begin by creating an empty Status class, and visually separating into sections:

Status class layout

If you’re wondering what’s the deal with the weird comment above the class name — it’s JSDoc, a helpful tool for documenting your code. If you’re not familiar with it, do check it out!

Next, let’s declare the fields each status will have and the constructor:

Status constructor

(I’m using parameter properties, since the class uses the values passed to the constructor as the values for its fields; if you have more complex logic, I’d suggest adding a “fields” section and declaring the fields there).

Note that the constructor is private — this makes it hidden, preventing the creation of new Status objects from outside. Often, classes that make the constructor private provide an alternative, custom method for creating instances of the class (such as a static factory), but the point of an enum is not to be able to create instances dynamically, so we’re keeping everything private.

Also, note the readonly modifier on each field — as the name suggests, the readonly modifier prevents the instances being changed from outside. As explained above (feature #7), if you would indeed prefer a field to be mutable — remove its readonly modifier.

Now that the fields are in place, we can declare the instances themselves:

Defining the instances

Again, note the readonly modifiers — they prevent setting the instances themselves to different values completely, I.E. running a command such as

outside of the class.

As for the naming convention, I personally go by the Java convention for naming enums — like other final variables, they are given uppercase names, with words separated by underscores.

With the instances in place, we can write the .values getter:

We’re using a getter so that accessing the values feels like a property — we write Status.values, not Status.values(). Behind the scenes, however, it runs this function exactly.

Note that we have to use “this” to access the static properties — that’s just how Javascript, and Typescript, are. That’s also the reason we can’t use a simple, readonly array

With this, our core functionality is in place.

For convenience, let’s add deserialization (using a static fromString() method) and serialization (by overriding the toJSON method):

deserialization (fromString())
serialization(toJSON())

And with that, our Status enum is done! In your own enums, you might have a need for more methods or properties — that depends on the use case. But adding them is as simple as writing the features, as we’ve done above.

Let’s look at the complete class:

Status enum — complete

And, running some check to see everything works:

Testing our class

The tests above (along with the Status class, of course) can be found here.

Generalizing

Refactoring the example above, we can get a basic template for the enum pattern:

General Enum example

The source code is also available here.

This template could be easily made into a snippet, and inserted easily whenever you would like. To save you time, you can find a VSCode JSON snippet format of the template here. A guide to adding (and generally creating) snippets to VSCode can be found here.

Conclusion

In this article, we learned why and when enums can be useful, and how to implement an advanced enum pattern in Typescript. We used private and readonly modifiers to configure accessibility from outside — which can be useful in many other scenarios.

In a follow-up article I’ll be exploring the enum pattern in plain Javascript — which required much more thought and a few advanced tricks, since Javascript does not have access / readonly modifiers (yet). There, we’ll take a dive into some more advanced Javascript functions, APIs and mechanics which one might be less familiar with. I encourage you to check it out!

I hope you enjoyed this article, and would love to hear your questions or feedback.

--

--

Nitzan Hen
The Startup

Full stack developer & undergrad student of Mathematics; very passionate about both. Enthusiastic about learning, teaching, writing, open source & more.