Enums are quite handy to define a set of named constants. In the TypeScript world, enums hold a unique position as both a type and a value.
However, enum comes with drawbacks...
Enums and const enums, are an exception to the Typescript structural typing. Meaning that Level1.INFO !== Level2.INFO !== 'INFO' even when both Level1.Info and Level2.INFO are set to 'INFO'.
Enums are not necessarily enumerable at runtime, and
Object.values(MyEnums)
can have unexpected results.Enums and const enums, are more a legacy feature than a recommended one.
Let's take an example:
enum Level {
VERBOSE = 'VERBOSE',
NORMAL = 'NORMAL',
DEBUG = 'DEBUG'
}
function log(level: Level) {}
log('DEBUG') // π₯ Argument of type '"DEBUG"' is not assignable to parameter of type 'Level'
log(Level.DEBUG) // β
const allLevels = Object.values(Level)
// allLevels = ["NONE", 0, "VERBOSE", "NORMAL", "DEBUG"] π€
While it operates as intended, the behavior of enums may seem somewhat strange to a TypeScript user.
The alternative solution
Fortunately, TypeScript offers features that, when combined, provide similar functionality to enums:
- const object
- union type
- value and a type can coexist with the same name
This leads to the following code that "fixed" the unwanted behavior of enums:
const Level = {
NONE : 0,
VERBOSE: 'VERBOSE',
NORMAL: 'NORMAL',
DEBUG: 'DEBUG'
} as const
type Level = (typeof Level)[keyof typeof Level]
function log(level: Level) {}
log('DEBUG') // β
log(Level.DEBUG) // β
const allLevels = Object.values(Level)
// [0, "VERBOSE", "NORMAL", "DEBUG"] β
The clear advantage of this approach is that, in most cases, it's possible to seamlessly replace enums without modifying existing code that uses them.
Additionally, the union type can be simplified using the utility type:
type UnionOfPropertyTypes<T extends object> = T[keyof T]
Resulting in a cleaner declaration:
const Level = {
NONE : 0,
VERBOSE: 'VERBOSE',
NORMAL: 'NORMAL',
DEBUG: 'DEBUG'
} as const
type Level = UnionOfPropertyTypes<typeof Level>
Top comments (0)