JavaScript does not have enums, so this post's title is a trick question and the answer is NO.
So if JavaScript doesn't have enums, we should probably define what we are looking for, so we know what to build. I will skip type unions as this a quite the stretch from no enums
- Enumeration define a fixed number of constants
- Uniqueness prevent any potential clashes or duplicate values.
- Immutability cannot be modified or re-assigned. Once created, they retain their identity throughout the code execution.
Enums in TypeScript are a widely covered topic. The generally accepted advice is that TypeScript Enums are not JavaScript, they were a mistake, and you should not use them. This is detailed in Object vs Enums in the TypeScript docs, and many videos and blog posts. If you want to use TypeScript enums, this post doesn't cover them.
String type unions are a common option, and one I quite enjoy. There are benefits to centralising enum defintion, we will cover those apporaches. JavaScript replacements for TypeScript enums.
Our starting point is the Plain Old JavaScript Object (known as the acronym POJO)
const LogLevel = {
Debug: "DEBUG",
Warning: "WARNING",
Error: "ERROR",
} as const;
type LogLevel = (typeof LogLevel)[keyof typeof LogLevel];
This is a nice drop in JavaScript replacement for a TypeScript enums.
- Enumeration β
- Uniqueness β
- Immutability π€· guaranteed at build-time, so it depends
Object.Freeze
instead of as const
We want our enums to be immutable, so we put as const
on our object to ensure
our object is not modifed.
As you hopefully know, const
is not constant in JavaScript π, so we use
as const
π to keep a const constant for real this time π
Let's take a look at one of the main often quoted reasons for not using TypeScript Enums and using a POJO.
Emit clean, idiomatic, recognizable JavaScript code.
TypeScript Design Goals
TypeScript enums breaks this principle, but having to compiles to something not recognizable. JavaScript doesn't have enums, so there if TypeScript wants to support enums, they cannot only be a type, code must be generated.
TypeScript provides better type-checking, but it still compiles to JavaScript and does not enforce the same level of safety at runtime. Why use a TypeScript feature when we can use a JavaScript feature and get build-time and runtime safety around modification of enums.
The alternative is to use
[Object.freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze
In standard JavaScript, you can use Object.freeze(obj)
to create read-only
objects and prevent modifications. By freezing an object representing your
enum-like structure, you can enforce immutability and prevent accidental
modifications.
const LogLevel = Object.freeze({
Debug: "DEBUG",
Warning: "WARNING",
Error: "ERROR",
});
type LogLevel = (typeof LogLevel)[keyof typeof LogLevel];
By using Object.freeze(), you ensure that the properties of the object cannot be
modified after it's defined. This helps in preventing unintended changes to the
enum-like structure. TypeScript understands what Object.freeze
does, so our
types are very similar to our as const
version.
- Enumeration β
- Uniqueness β
- Immutability β
Symbol
instead of string
While these approaches can emulate enums in JavaScript, they lack some of the strict type-checking and safety features found in languages with built-in enum support. There is still something strange about our enums so far. They are not unique. An enum can equal another enum, which can equal a stringβ½
Using the example of two enums that happen to match. Let's say you are writing a linter. You define log levels like before, and also a lint level.
Is it accurate to say that LogLevel.Warning
is the same as
LintLevel.Warning
. Should they both be equal to "WARNING"
const LogLevel = Object.freeze({
Debug: "DEBUG",
Warning: "WARNING",
Error: "ERROR",
});
type LogLevel = (typeof LOG_LEVEL)[keyof typeof LOG_LEVEL];
const LintLevel = Object.freeze({
Warning: "WARNING",
Error: "ERROR",
});
type LintLevel = (typeof LintLevel)[keyof typeof LintLevel];
They both have a WARNING and ERROR value. let's say we had two functions log
and lint
function log(level: LogLevel) {}
function lint(level: LintLevel) {}
log(LintLevel.Warning);
lint(LogLevel.Error);
This is completely fine, our program runs, but is it really a valid state. Are these the same values, or do they just happen to have the same name?
How can we ensure the enums are unique?
Symbol to the rescue. Symbols are guaranteed to be unique, so we can use them as our enum value
const LogLevelDebug = Symbol("Debug");
const LogLevelWarning = Symbol("Warning");
const LogLevelError = Symbol("Error");
const LogLevel = Object.freeze({
Debug: LogLevelDebug,
Warning: LogLevelWarning,
Error: LogLevelError,
});
There is a trade-off here, you will have write a function to parse strings and convert these symbols into strings, if required. You will also no longer be able to use the string values directly, which can be seen as positive or negative. Centralising enum definition is certainly good, but it is nice to not have to import an enum to use a function.
- Enumeration β
- Uniqueness β
- Immutability β
- Extra code for parsing, mapping to a string β
If the JavaScript Records & Tuples Proposal is accepted we will be able write
const LogLevel = #{
Debug: Symbol("Debug"),
Warning: Symbol("Warning"),
Error: Symbol("Error"),
}
Thanks for reading, happy coding!
Conclusion
While JavaScript lacks native enum support, we can use techniques like freezing objects and symbols to emulate enum-like behavior. These approaches provide varying runtime safety, immutability and uniqueness. Understand these workarounds and choose an approach that fits your needs.