Skip to content

Disallow the use of enums #417

@mcmire

Description

@mcmire

We have had multiple discussions in the past on disallowing the use of enums. We should follow through with this.

The case for enums

The popular argument for needing enums is this: if you have a collection of related values and you want to change one of them, you can easily do so without needing to update all instances of that value across your entire codebase.

Say you need to define a set of acceptable HTTP methods. You might use an enum to declare this:

enum HttpMethod {
  Get = 'GET',
  Post = 'POST',
}

Now say you have a bunch of places in your code that reference one of these values, e.g. HttpMethod.Post, and you want to change one of the values (e.g. 'post' instead of 'POST'). If you were using literal strings, you'd have to go and change all of them, but now you could merely change it in the enum, and everything would "just work".

The case against enums

Enums have a surprisingly large number of features and quirks that are bloat at best and dangerous at worst.

The biggest problem with enums — the one that affects us most — is that they are examples of nominal typing. This means that if you have two enums with the same exact contents, they are treated by TypeScript as two distinct types and are not assignable to each other. This is not how TypeScript works usually! Everything else uses structurable typing: if two types have the same members, they are the same type.

Structural typing is super convenient because it means adding a new property to an object type is a non-breaking change. But this is not the case with enums! There are several instances — in not only this repo but in other repos — where we've introduced breaking changes accidentally just by changing an enum. Even releasing a new version of a package (such as utils) that contains an enum can be breaking in a sense: if the package expects dependents to use a specific version of an enum and they use an older version instead, a type error will be produced. This introduces unnecessary friction.

Here are some other extraneous, strange, or outright annoying things about enums:

Looking ahead

As of 22.18, Node can run TypeScript code using type erasure, obviating the need to run TypeScript through a transform step (Babel, SWC, esbuild, or whatever the "fast TypeScript runner du jour" is). If we used this, it could theoretically save a bunch of time in CI and remove some tooling bloat. We could also turn on the --erasableSyntaxOnly option introduced in TypeScript 5.8 to enforce this.

To make use of this, we would need to stop using enums.

How to stop using enums

This could be as simple as adding a lint rule:

{
  "rules": {
    "no-restricted-syntax": [
      "error",
      {
        "selector": "TSEnumDeclaration",
        "message": "Don't use enums. There are a number of reasons why enums are problematic, but the most important is that because they are treated nominally, not structurally, they make it very easy to accidentally introduce breaking changes. Instead, use an object + type, an array + type, or just a type. Learn more here: https://github.com/MetaMask/eslint-config/issues/417"
      }
    ]
  }
}

But if not enums, then what else? This guide outlines a few ways, but essentially there would be three options:

  • If accessing individual enum members is important, use a literal object + as const + type. This might look something like:
    const HTTP_METHODS = {
      Get: 'GET',
      Post: 'POST',
    } as const;
    type HttpMethod = (typeof HTTP_METHODS)[keyof typeof HTTP_METHODS];
  • If accessing enum members is not important, but iterating over enum values is, use a literal array + as const + a type union:
    const HTTP_METHODS = ['GET', 'POST'] as const;
    type HttpMethod = (typeof HTTP_METHODS)[number];
  • If none of the above, just use a union type:
    type HttpMethod = 'GET' | 'POST'

Caveats

  • If we switch away from enums we may not be able to enforce member names as we do now.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions