Skip to content

Ban enums, and replace them with alternatives#280

Open
mcmire wants to merge 1 commit intomainfrom
ban-enums
Open

Ban enums, and replace them with alternatives#280
mcmire wants to merge 1 commit intomainfrom
ban-enums

Conversation

@mcmire
Copy link
Copy Markdown
Contributor

@mcmire mcmire commented Apr 16, 2026

There are a number of reasons why enums are bad, but the biggest problem — the one that affects us most — is that enums behave differently than the rest of TypeScript: while TypeScript is usually a structural type system, enums break the mold and are treated as nominal types.

This creates unintentional type errors. For instance, if one package has a function which takes an argument of type KnownCaipNamespace from @metamask/utils 11.1.0, and another package attempts to call that function with a member of KnownCaipNamespace from @metamask/utils 11.0.0, then a type error would be produced, even if the enums across both versions have the same exact contents. The second package would need to bump to 11.1.0 to fix the error.

There are other strange and undesirable "features" of enums that we do use. These are detailed here:
MetaMask/eslint-config#417

To prevent these kinds of problems from occurring in the future, this commit adds a lint rule to ban the use of enums, and updates all instances of enums to use object literals and types instead.


Note

Medium Risk
Introduces breaking type changes to exported constants that may require downstream TypeScript updates, though runtime access patterns like Duration.Second should remain compatible.

Overview
This PR bans TypeScript enum usage via an ESLint no-restricted-syntax rule for TSEnumDeclaration to prevent future enum additions.

It replaces the exported enums KnownCaipNamespace, JsonSize, and Duration with as const object literals plus derived union types, and updates the type tests/usages accordingly (e.g., Object.values(Duration) now yields typed numeric values). The changelog is updated to call out the breaking type-level impact for consumers relying on the enum types in signatures.

Reviewed by Cursor Bugbot for commit f112624. Bugbot is set up for automated code reviews on this repo. Configure here.

There are a number of reasons why enums are bad, but the biggest problem
— the one affects us most — is that enums behave differently than the
rest of TypeScript: while TypeScript is usually a **structural type
system**, enums break the mold and are treated as **nominal types**.

This creates unintentional type errors. For instance, if one package has
a function which takes an argument of type `KnownCaipNamespace` from
`@metamask/utils` 11.1.0, and another package attempts to call that
function with a member of `KnownCaipNamespace` from `@metamask/utils`
11.0.0, then a type error would be produced, even if the enums across
both versions have the same exact contents. The second package would
need to bump to 11.1.0 to fix the error.

There are other strange and undesirable "features" of enums that we do
use. These are detailed here:
MetaMask/eslint-config#417

To prevent these kinds of problems from occurring in the future, this
commit adds a lint rule to ban the use of enums, and updates all
instances of enums to use object literals and types instead.
@mcmire mcmire marked this pull request as ready for review April 16, 2026 19:29
Comment thread src/caip-types.ts
Comment on lines +148 to +149
export type KnownCaipNamespace =
(typeof KnownCaipNamespace)[keyof typeof KnownCaipNamespace];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, at first glance it seems like this would create a circular type reference, but is it actually referring to the object KnownCaipNamespace here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's right. If this had been written from scratch then I would have used KNOWN_CAIP_NAMESPACES for the variable and KnownCaipNamespace for the type. I wanted to minimize breaking changes here, but if you think it's going to be too confusing I can change it. What do you think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine. Wondering if it makes sense to add a simple helper for this though, maybe it makes it a little bit easier to understand?

Suggested change
export type KnownCaipNamespace =
(typeof KnownCaipNamespace)[keyof typeof KnownCaipNamespace];
export type ObjectToUnion<Type> = Type[keyof Type];
export type KnownCaipNamespace = ObjectToUnion<typeof KnownCaipNamespace>;

Copy link
Copy Markdown
Contributor

@ccharly ccharly Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO that's neat. Value and type have matching names and they should not conflict. But I guess that's a personal opinion 😁

Comment thread .eslintrc.js
Comment on lines +10 to +19
rules: {
'no-restricted-syntax': [
'error',
{
selector: 'TSEnumDeclaration',
message:
"Don't use enums. There are a number of reasons why they are problematic, but the most important is that TypeScript treats them nominally, not structurally, and this can cause unexpected 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",
},
],
},
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should add this to the shared ESLint configs at some point.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you created an issue for this already, so disregard this comment. 😅

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We talked about enabling this in accounts too, but indeed if that's enforced by the shared eslint, that's even better 🚀

Copy link
Copy Markdown
Contributor

@ccharly ccharly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants