-
Notifications
You must be signed in to change notification settings - Fork 18
Disallow the use of enums #417
Description
We have had multiple discussions in the past on disallowing the use of enums. We should follow up on this.
Why enums are useful
Enums help in the case when you have a collection of possible values and you want to change one of them without needing to update all of the references.
The popular example is this. Say you need to define a set of acceptable HTTP methods. You can use an enum to declare this:
enum HttpMethod {
Get = 'GET',
Post = 'POST',
}And say you have a bunch of places in your code that reference one of these values, e.g. HttpMethod.Post.
But now say that you want to change the value, e.g. HttpMethod.Post is '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".
Enums also help when using a TypeScript also uses exhaustive checks with type unions, so it isn't specific to enums.switch statement to handle possibilities as TypeScript can enforce an exhaustive check.
Why enums are not useful
Enums have a surprisingly large number of features that are bloat at best and dangerous at worst.
- Numeric enums can be accessed in reverse.
const enumallows for inlining enums at compile time.
At the same time, there are a number of known annoyances and pitfalls with using enums:
- Numeric enums can easily be used in ways that break type safety (source 1, source 2).
- When using numeric enums, rearranging members breaks existing code.
- Ambient const enums are incompatible with
isolatedModules. - Inlining enums via
constcan cause surprising bugs.
Besides this, enums violate one of the fundamental rules of TypeScript, which is that TypeScript should only ever be a layer on top of TypeScript, and that it should be possible to remove that layer without affecting the underlying code. Everything in TypeScript works this way — except enums (this is even noted at the very top of the section on enums in the TypeScript handbook).
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
What's the alternative to enums? This guide outlines a few ways, but essentially we would have two options:
- 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];
- Use a literal array +
as const+ a type union:const HTTP_METHODS = ['GET', 'POST'] as const; type HttpMethod = (typeof HTTP_METHODS)[number];
We could also disallow enums via a lint rule:
{
"rules": {
"no-restricted-syntax": [
"error",
{
"selector": "TSEnumDeclaration",
"message": "Don't declare enums"
}
]
}
}Caveats
- If we switch away from enums we may not be able to enforce member names as we do now.