Typescript has unions, so are enums redundant

enumstypescriptunions

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?

Best Answer

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.