Typescript has unions, so are enums redundant?

TypescriptEnumsUnions

Typescript Problem Overview


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?

Typescript Solutions


Solution 1 - Typescript

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 - Typescript

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 - Typescript

There are few reasons you might want to use an enum

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 - Typescript

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 a string) 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.

Solution 5 - Typescript

The enum type is not redundant, but in most cases union is preferred.

But not always. Using enums to represents e.g state transitions could be much more handy and expressive than using union**

Consider real live scenario:

enum OperationStatus {
  NEW = 1,
  PROCESSING = 2,
  COMPLETED = 4
}

OperationStatus.PROCESSING > OperationStatus.NEW // true
OperationStatus.PROCESSING > OperationStatus.COMPLETED // false

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionprmphView Question on Stackoverflow
Solution 1 - TypescriptkimamulaView Answer on Stackoverflow
Solution 2 - TypescriptAmidView Answer on Stackoverflow
Solution 3 - TypescriptmatthewView Answer on Stackoverflow
Solution 4 - TypescriptRomain DeneauView Answer on Stackoverflow
Solution 5 - TypescriptJohn SmithView Answer on Stackoverflow