Typescript has unions, so are enums redundant?

后端 未结 5 1687
闹比i
闹比i 2020-12-01 00:22

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          


        
相关标签:
5条回答
  • 2020-12-01 01:06

    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)

    0 讨论(0)
  • 2020-12-01 01:21

    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 does not 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.

    0 讨论(0)
  • 2020-12-01 01:23

    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
    
    0 讨论(0)
  • 2020-12-01 01:25

    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.

    0 讨论(0)
  • 2020-12-01 01:27

    There are few reasons you might want to use an enum

    • You can iterate over an enum.
    • You can use an enum as flags. 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",
    } 
    
    0 讨论(0)
提交回复
热议问题