Typescript has unions, so are enums redundant?
Asked Answered
C

5

211

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?

Claudclauddetta answered 27/10, 2016 at 3:53 Comment(1)
Enums map to numbers, which I guess can be helpful in certain situations. I'm assuming they also want to eventually do more with enums, such as giving them properties you can call (like c# or java enums.)Milestone
T
103

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)

Trass answered 27/10, 2016 at 8:8 Comment(4)
I think you mean to say that there are reasons to use an enum. The first sentence is confusing. "As far as I see [enums] are not..." answering the questions "is [there] any reason to declare an enum type. "Inheritrix
in my reading, the first sentence's "As far as I see they are not" was referring to enums in the title question "... so are enums redundant?" . i'm submitting an edit to that extentUnionist
I would also that that enums "centralizes" values definitions where union with string literal NOT... if you change a value of a string literal you must change in every place where it was used, if you change a value of a enum then "automatically" it changes everywhere. I do like to have things not repeated, as repetition often shows a pattern, and patterns may be encapsulated in proper abstractions.Debut
"that are otherwise impossible with union types (like enumerating the possible enum values)" As @kimamula's answer shows, it is very easy to create iterable union types.Clamshell
M
252

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.

Mohler answered 3/2, 2020 at 14:59 Comment(10)
Ok cool. But what if you want to put this declaration in a shared d.ts typings file? You can only initialize a const to a string or numeric literal in an ambient contextClaudclauddetta
I don't think it's possible to iterate over something that is only declared in an ambient context. Providing both the declaration declare const permissions: readonly ['read', 'write', 'execute'] and the real JavaScript object const permissions = ['read', 'write', 'execute'] should work.Mohler
I do not want string literals to be assignable nor comparable to Enums. const foo: StringEnum === StringEnum.Foo // true or false I want this restriction to make sure we don't have string literal mixups.Inheritrix
@matthew Thanks for your comment. I updated my answer to be fair.Mohler
Great write-up! It may be worth adding a disadvantage of string unions types, though, which is related to your point on opaqueness: TypeScript does not distinguish between a constant string like 'read' being used in different union types. So when looking for references of this string, the references across all union types where it is used are shown. That my be the main reason why the "Rename Symbol" cannot be executed on those strings either.Alathia
> The union types pattern is much cooler @Mohler this is a weak argument. That's coming from a supporter of the pattern proposed in this otherwise very well-put answer.Conga
With enum, it is much easier to find all usages of a value in the project, for renaming, adding, removing new values, ... For me it is very convenient that my IDE gives a warning when I forget a case in a switch statement.Sepulveda
@Conga Thanks for your comment. I agree and removed that part.Mohler
Rather than "opaque" strings I would suggest using Symbols as they are unique (and therefore truly opaque) at runtime and cannot be used via any number or string literal. This forces parsing and stringifying to explicitly not rely on any in-memory, runtime identifier meant to be opaque too so that you force yourself to decouple a string representation of an object from runtime code symbols.Optometer
I don't agree with "you should prefer union types to enums". I think #1 is opinionated. The TS documentation on const enums offers a solution for #2 using preserveConstEnums and a build step, and the vast majority of programmers aren't writing libraries anyway. #3 is a misuse and can be avoided, and #4 isn't an issue at all given how you discuss the advantages of having an enum as a single source of truth afterwards. Perhaps it is better to just read the TS documentation on enums and const enums and be aware of the important issues rather than subscribe solely to one or the other.Foreskin
T
103

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)

Trass answered 27/10, 2016 at 8:8 Comment(4)
I think you mean to say that there are reasons to use an enum. The first sentence is confusing. "As far as I see [enums] are not..." answering the questions "is [there] any reason to declare an enum type. "Inheritrix
in my reading, the first sentence's "As far as I see they are not" was referring to enums in the title question "... so are enums redundant?" . i'm submitting an edit to that extentUnionist
I would also that that enums "centralizes" values definitions where union with string literal NOT... if you change a value of a string literal you must change in every place where it was used, if you change a value of a enum then "automatically" it changes everywhere. I do like to have things not repeated, as repetition often shows a pattern, and patterns may be encapsulated in proper abstractions.Debut
"that are otherwise impossible with union types (like enumerating the possible enum values)" As @kimamula's answer shows, it is very easy to create iterable union types.Clamshell
I
39

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",
} 
Inheritrix answered 31/3, 2017 at 18:26 Comment(2)
Am I the only one that thinks using enum for "bit flags" is an anti-pattern?Tamanaha
Depends on what you're doing. Bit-twiddling isn't as rare as it used to be in javascript.Ygerne
D
20

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.

Derry answered 1/4, 2019 at 16:38 Comment(0)
K
3

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
Keown answered 27/12, 2019 at 23:15 Comment(3)
I think if you do this you should explicitly assign the number values in the enum declaration to prevent a reordering (e.g. because someone else thinks it would make more sense to have them alphabetically) from introducing a runtime error that might be overlooked.Bartley
I would call it a code smell if I need to compare status of my operations like above.Outride
I don't think it's a code smell. The server does not need to know how you handle the data in the front if it just serves it. This leaves the Front End the responsibility of deciding how to use the data and to change it in the future if needed without requiring any work on the server. Moreover this is useful in cases where you don't want to assign a word to describe a state. A number is perfect to describe states. Another example could be account permissions. Rather than storing "admin", you can just store 0.Fokos

© 2022 - 2024 — McMap. All rights reserved.