0025 - Deprecate TypeScript Enum Types
ID: | ADR-0025 |
---|---|
Status: | PROPOSED |
Published: | 2025-05-30 |
Context and Problem Statement
TypeScript experts have long discouraged the use of enums. There are a variety of cited reasons. Among them:
- Enums that are not
const
substantially increase output size. - Numeric enums implicitly cast from
number
to the enum type, which allows invalid enum values to be transmitted to functions. - String enums are named types that require a member is used, which is inconsistent with the behavior of numeric enums.
These inconsistencies cause increased complexity from guard statements in the best of cases. In the worst cases, their limitations may be unknown, and thus unguarded.
Detection
TypeScript deprecation can be linted using a fairly short ESLint plugin. The code has already been
contributed to main and is configured as an error-level
lint. The same PR adds FIXME
comments for each team to address.
The Enum-like Pattern
In most cases, enums are unnecessary. A readonly (as const
) object coupled with a type alias
avoids both code generation and type inconsistencies.
// declare the raw data and reduce repetition with an internal type
const _CipherType = {
Login: 1,
SecureNote: 2,
Card: 3,
Identity: 4,
SshKey: 5,
} as const;
type _CipherType = typeof _CipherType;
// derive the enum-like type from the raw data
export type CipherType = _CipherType[keyof _CipherType];
// assert that the raw data is of the enum-like type
export const CipherType: Readonly<{ [K in keyof _CipherType]: CipherType }> =
Object.freeze(_CipherType);
This code creates a type CipherType
that allows arguments and variables to be typed similarly to
an enum. It also strongly types the const CiperType
so that direct accesses of its members
preserve type safety. This ensures that type inference properly limits the accepted values to those
allowed by type CipherType
. Without the type assertion, the compiler infers number
in these
cases:
const s = new Subject(CipherType.Login); // `s` is a `Subject<CipherType>`
const a = [CipherType.Login, CipherType.Card]; // `a` is an `Array<CipherType>`
const m = new Map([[CipherType.Login, ""]]); // `m` is a `Map<CipherType, string>`
- Types that use enums like computed property names issue a compiler error with this pattern. This issue is fixed as of TypeScript 5.8.
- Certain objects are more difficult to create with this pattern. This is explored in Appendix A.
Considered Options
- Allow enums, but advise against them - Allow enum use to be decided on a case-by-case basis by the team that owns the enum. Reduce the level of the lint to a "suggestion".
- Deprecate enum use - Allow enums to exist for historic or technical purposes, but prohibit the introduction of new ones. Reduce the lint to a "warning" and allow the lint to be disabled.
- Eliminate enum use - This is the current state of affairs. Prohibit the introduction of any new enum and replace all enums in the codebase with typescript objects. Prohibit disabling of the lint.
Decision Outcome
Chosen option: Deprecate enum use
Positive Consequences
- Allows for cases where autogenerated code introduces an enum by necessity.
- Developers receive a warning in their IDE to discourage new enums.
- The warning can direct them to our contributing docs, where they can learn typesafe alternatives.
- Our compiled code size decreases when enums are replaced.
- If all teams eliminate enums in practice, the warning can be increased to an error.
Negative Consequences
- Unnecessary usage may persist indefinitely on teams carrying a high tech debt.
- The lint increased the number of FIXME comments in the code by about 10%.
Plan
- Update contributing docs with patterns and best practices for enum replacement.
- Update the reporting level of the lint to "warning".
Appendix A: Mapped Types and Enum-likes
Mapped types cannot determine that a mapped enum-like object is fully assigned. Code like the following causes a compiler error:
const instance: Record<CipherType, boolean> = {
[CipherType.Login]: true,
[CipherType.SecureNote]: false,
[CipherType.Card]: true,
[CipherType.Identity]: true,
[CipherType.SshKey]: true,
};
Why does this happen?
The members of const _CipherType
all have a literal type. _CipherType.Login
, for
example, has a literal type of 1
. type CipherType
maps over these members, aggregating them into
the structural type 1 | 2 | 3 | 4 | 5
.
const CipherType
asserts its members have type CipherType
, which overrides the literal types the
compiler inferred for the member in const _CipherType
. The compiler sees the type of
CipherType.Login
as type CipherType
(which aliases 1 | 2 | 3 | 4 | 5
).
Now consider a mapped type definition:
// `MappedType` is structurally identical to Record<CipherType, boolean>
type MappedType = { [K in CipherType]: boolean };
When the compiler examines instance
, it only knows that the type of each of its members is
CipherType
. That is, the type of instance
to the compiler is
{ [K in 1 | 2 | 3 | 4 | 5]?: boolean }
. This doesn't sufficiently overlap with MappedType
, which
is looking for { [1]: boolean, [2]: boolean, [3]: boolean, [4]: boolean, [5]: boolean }
. The
failure occurs, because the inferred type can have fewer fields than MappedType
.
Workarounds
Option A: Assert the type is correct. You need to manually verify this. The compiler cannot typecheck it.
const instance: MappedType = {
[CipherType.Login]: true,
// ...
} as MappedType;
Option B: Define the mapped type as a partial. Then, inspect its properties before using them.
type MappedType = { [K in CipherType]?: boolean };
const instance: MappedType = {
[CipherType.Login]: true,
// ...
};
if (CipherType.Login in instance) {
// work with `instance[CipherType.Login]`
}
Option C: Use a collection. Consider this approach when downstream code reflects over the result
with in
or using methods like Object.keys
.
const collection = new Map([[CipherType.Login, true]]);
const instance = collection.get(CipherType.Login);
if (instance) {
// work with `instance`
}
const available = [CipherType.Login, CipherType.Card];
if (available.includes(CipherType.Login)) {
// ...
}