Typescript has unions, so are enums redundant?
Ever since TypeScript introduced unions types, I wonder if there is any reason to declare an enum type. Consider the following enum type declaration:
enum X { A, B, C }
var x: X = X.A;
and a similar union type declaration:
type X: "A" | "B" | "C"
var x: X = "A";
If they basically serve the same purpose, and unions are more powerful and expressive, then why are enums necessary?
Solution 1:
With the recent versions of TypeScript, it is easy to declare iterable union types. Therefore, you should prefer union types to enums.
How to declare iterable union types
const permissions = ['read', 'write', 'execute'] as const;
type Permission = typeof permissions[number]; // 'read' | 'write' | 'execute'
// you can iterate over permissions
for (const permission of permissions) {
// do something
}
When the actual values of the union type do not describe theirselves very well, you can name them as you do with enums.
// when you use enum
enum Permission {
Read = 'r',
Write = 'w',
Execute = 'x'
}
// union type equivalent
const Permission = {
Read: 'r',
Write: 'w',
Execute: 'x'
} as const;
type Permission = typeof Permission[keyof typeof Permission]; // 'r' | 'w' | 'x'
// of course it's quite easy to iterate over
for (const permission of Object.values(Permission)) {
// do something
}
Do not miss as const
assertion which plays the crucial role in these patterns.
Why it is not good to use enums?
1. Non-const enums do not fit to the concept "a typed superset of JavaScript"
I think this concept is one of the crucial reasons why TypeScript has become so popular among other altJS languages. Non-const enums violate the concept by emitting JavaScript objects that live in runtime with a syntax that is not compatible with JavaScript.
2. Const enums have some pitfalls
Const enums cannot be transpiled with Babel
There are currently two workarounds for this issue: to get rid of const enums manually or with plugin babel-plugin-const-enum
.
Declaring const enums in an ambient context can be problematic
Ambient const enums are not allowed when the --isolatedModules
flag is provided.
A TypeScript team member says that "const enum
on DT really does not make sense" (DT refers to DefinitelyTyped) and "You should use a union type of literals (string or number) instead" of const enums in ambient context.
Const enums under --isolatedModules
flag behave strangely even outside an ambient context
I was surprised to read this comment on GitHub and confirmed that the behavior is still true with TypeScript 3.8.2.
3. Numeric enums are not type safe
You can assign any number to numeric enums.
enum ZeroOrOne {
Zero = 0,
One = 1
}
const zeroOrOne: ZeroOrOne = 2; // no error!!
4. Declaration of string enums can be redundant
We sometimes see this kind of string enums:
enum Day {
Sunday = 'Sunday',
Monday = 'Monday',
Tuesday = 'Tuesday',
Wednesday = 'Wednesday',
Thursday = 'Thursday',
Friday = 'Friday',
Saturday = 'Saturday'
}
I have to admit that there is an enum feature that cannot be achieved by union types
Even if it is obvious from the context that the string value is included in the enum, you cannot assign it to the enum.
enum StringEnum {
Foo = 'foo'
}
const foo1: StringEnum = StringEnum.Foo; // no error
const foo2: StringEnum = 'foo'; // error!!
This unifies the style of enum value assignment throughout the code by eliminating the use of string values or string literals. This behavior is not consistent with how TypeScript type system behaves in the other places and is kind of surprising and some people who thought this should be fixed raised issues (this and this), in which it is repeatedly mentioned that the intent of string enums is to provide "opaque" string types: i.e. they can be changed without modifying consumers.
enum Weekend {
Saturday = 'Saturday',
Sunday = 'Sunday'
}
// As this style is forced, you can change the value of
// Weekend.Saturday to 'Sat' without modifying consumers
const weekend: Weekend = Weekend.Saturday;
Note that this "opaqueness" is not perfect as the assignment of enum values to string literal types is not limited.
enum Weekend {
Saturday = 'Saturday',
Sunday = 'Sunday'
}
// The change of the value of Weekend.Saturday to 'Sat'
// results in a compilation error
const saturday: 'Saturday' = Weekend.Saturday;
If you think this "opaque" feature is so valuable that you can accept all the drawbacks I described above in exchange for it, you cannot abandon string enums.
How to eliminate enums from your codebase
With the no-restricted-syntax
rule of ESLint, as described.
Solution 2:
As far as I see they are not redundant, due to the very simple reason that union types are purely a compile time concept whereas enums are actually transpiled and end up in the resulting javascript (sample).
This allows you to do some things with enums, that are otherwise impossible with union types (like enumerating the possible enum values)
Solution 3:
There are few reasons you might want to use an enum
- You can iterate over an
enum
. - You can use an
enum
asflags
. Bit Flags - Here are some use cases. Enums TypeScript Deep Dive.
I see the big advantages of using a union is that they provide a succinct way to represent a value with multiple types and they are very readable.
let x: number | string
EDIT: As of TypeScript 2.4 Enums now support strings.
enum Colors {
Red = "RED",
Green = "GREEN",
Blue = "BLUE",
}
Solution 4:
Enums can be seen conceptually as a subset of union types, dedicated to int
and/or string
values, with a few additional features mentioned in other responses that make them friendly to use, e.g. namespace.
Regarding type safety, numeric enums are the less safe, then come union types and finally string enums:
// Numeric enum
enum Colors { Red, Green, Blue }
const c: Colors = 100; // ⚠️ No errors!
// Equivalent union types
type Color =
| 0 | 'Red'
| 1 | 'Green'
| 2 | 'Blue';
let color: Color = 'Red'; // ✔️ No error because namespace free
color = 100; // ✔️ Error: Type '100' is not assignable to type 'Color'
type AltColor = 'Red' | 'Yellow' | 'Blue';
let altColor: AltColor = 'Red';
color = altColor; // ⚠️ No error because `altColor` type is here narrowed to `"Red"`
// String enum
enum NamedColors {
Red = 'Red',
Green = 'Green',
Blue = 'Blue',
}
let namedColor: NamedColors = 'Red'; // ✔️ Error: Type '"Red"' is not assignable to type 'Colors'.
enum AltNamedColors {
Red = 'Red',
Yellow = 'Yellow',
Blue = 'Blue',
}
namedColor = AltNamedColors.Red; // ✔️ Error: Type 'AltNamedColors.Red' is not assignable to type 'Colors'.
More on that topic in this 2ality article: TypeScript enums: How do they work? What can they be used for?
Union types support heterogenous data and structures, enabling polymorphism for instance:
class RGB {
constructor(
readonly r: number,
readonly g: number,
readonly b: number) { }
toHSL() {
return new HSL(0, 0, 0); // Fake formula
}
}
class HSL {
constructor(
readonly h: number,
readonly s: number,
readonly l: number) { }
lighten() {
return new HSL(this.h, this.s, this.l + 10);
}
}
function lightenColor(c: RGB | HSL) {
return (c instanceof RGB ? c.toHSL() : c).lighten();
}
In between enums and union types, singletons can replace enums. It's more verbose but also more object-oriented:
class Color {
static readonly Red = new Color(1, 'Red', '#FF0000');
static readonly Green = new Color(2, 'Green', '#00FF00');
static readonly Blue = new Color(3, 'Blue', '#0000FF');
static readonly All: readonly Color[] = [
Color.Red,
Color.Green,
Color.Blue,
];
private constructor(
readonly id: number,
readonly label: string,
readonly hex: string) { }
}
const c = Color.Red;
const colorIds = Color.All.map(x => x.id);
I tend to look at F# to see good modeling practices. A quote from an article on F# enums on F# for fun and profit that can be useful here:
In general, you should prefer discriminated union types over enums, unless you really need to have an
int
(or astring
) value associated with them
There are other alternatives to model enums. Some of them are well described in this other 2ality article Alternatives to enums in TypeScript.