11---
22adr : " 0025"
3- status : Proposed
3+ status : Accepted
44date : 2025-05-30
55tags : [clients, typescript]
66---
@@ -35,43 +35,34 @@ In most cases, enums are unnecessary. A readonly (`as const`) object coupled wit
3535avoids both code generation and type inconsistencies.
3636
3737``` ts
38- // declare the raw data and reduce repetition with an internal type
39- const _CipherType = {
38+ const CipherType = Object .freeze ({
4039 Login: 1 ,
4140 SecureNote: 2 ,
4241 Card: 3 ,
4342 Identity: 4 ,
4443 SshKey: 5 ,
45- } as const ;
44+ } as const ) ;
4645
47- type _CipherType = typeof _CipherType ;
48-
49- // derive the enum-like type from the raw data
50- export type CipherType = _CipherType [keyof _CipherType ];
51-
52- // assert that the raw data is of the enum-like type
53- export const CipherType: Readonly <{ [K in keyof _CipherType ]: CipherType }> =
54- Object .freeze (_CipherType );
46+ export type CipherType = _CipherType [keyof typeof CipherType ];
5547```
5648
5749This code creates a ` type CipherType ` that allows arguments and variables to be typed similarly to
58- an enum. It also strongly types the ` const CiperType ` so that direct accesses of its members
59- preserve type safety. This ensures that type inference properly limits the accepted values to those
60- allowed by ` type CipherType ` . Without the type assertion, the compiler infers ` number ` in these
61- cases:
62-
63- ``` ts
64- const s = new Subject (CipherType .Login ); // `s` is a `Subject<CipherType>`
65- const a = [CipherType .Login , CipherType .Card ]; // `a` is an `Array<CipherType>`
66- const m = new Map ([[CipherType .Login , " " ]]); // `m` is a `Map<CipherType, string>`
67- ```
50+ an enum.
6851
6952::: warning
7053
71- - Types that use enums like [ computed property names] [ computed-property-names ] issue a compiler
72- error with this pattern. [ This issue is fixed as of TypeScript 5.8] [ no-member-fields-fixed ] .
73- - Certain objects are more difficult to create with this pattern. This is explored in
74- [ Appendix A] ( #appendix-a-mapped-types-and-enum-likes ) .
54+ Unlike an enum, TypeScript lifts the type of the members of ` const CipherType ` to ` number ` . Code
55+ like the following requires you explicitly type your variables:
56+
57+ ``` ts
58+ // ✅ Do: strongly type enum-likes
59+ const subject = new Subject <CipherType >();
60+ let value: CipherType = CipherType .Login ;
61+
62+ // ❌ Do not: use type inference
63+ const array = [CipherType .Login ]; // infers `number[]`
64+ let value = CipherType .Login ; // infers `1`
65+ ```
7566
7667:::
7768
@@ -82,7 +73,7 @@ const m = new Map([[CipherType.Login, ""]]); // `m` is a `Map<CipherType, string
8273- ** Deprecate enum use** - Allow enums to exist for historic or technical purposes, but prohibit the
8374 introduction of new ones. Reduce the lint to a "warning" and allow the lint to be disabled.
8475- ** Eliminate enum use** - This is the current state of affairs. Prohibit the introduction of any
85- new enum and replace all enums in the codebase with typescript objects. Prohibit disabling of the
76+ new enum and replace all enums in the codebase with TypeScript objects. Prohibit disabling of the
8677 lint.
8778
8879## Decision Outcome
@@ -92,6 +83,8 @@ Chosen option: **Deprecate enum use**
9283### Positive Consequences
9384
9485- Allows for cases where autogenerated code introduces an enum by necessity.
86+ - Literals (e.g. ` 1 ` ) convert to the enum-like type with full type safety.
87+ - Works with mapped types such as ` Record<T, U> ` and discriminated unions.
9588- Developers receive a warning in their IDE to discourage new enums.
9689- The warning can direct them to our contributing docs, where they can learn typesafe alternatives.
9790- Our compiled code size decreases when enums are replaced.
@@ -101,98 +94,13 @@ Chosen option: **Deprecate enum use**
10194
10295- Unnecessary usage may persist indefinitely on teams carrying a high tech debt.
10396- The lint increased the number of FIXME comments in the code by about 10%.
97+ - Enum-likes cannot be referenced by angular templates
10498
10599### Plan
106100
107101- Update contributing docs with patterns and best practices for enum replacement.
108102- Update the reporting level of the lint to "warning".
109103
110- ## Appendix A: Mapped Types and Enum-likes
111-
112- Mapped types cannot determine that a mapped enum-like object is fully assigned. Code like the
113- following causes a compiler error:
114-
115- ``` ts
116- const instance: Record <CipherType , boolean > = {
117- [CipherType .Login ]: true ,
118- [CipherType .SecureNote ]: false ,
119- [CipherType .Card ]: true ,
120- [CipherType .Identity ]: true ,
121- [CipherType .SshKey ]: true ,
122- };
123- ```
124-
125- #### Why does this happen?
126-
127- The members of ` const _CipherType ` all have a [ literal type] [ literal-type ] . ` _CipherType.Login ` , for
128- example, has a literal type of ` 1 ` . ` type CipherType ` maps over these members, aggregating them into
129- the structural type ` 1 | 2 | 3 | 4 | 5 ` .
130-
131- ` const CipherType ` asserts its members have ` type CipherType ` , which overrides the literal types the
132- compiler inferred for the member in ` const _CipherType ` . The compiler sees the type of
133- ` CipherType.Login ` as ` type CipherType ` (which aliases ` 1 | 2 | 3 | 4 | 5 ` ).
134-
135- Now consider a mapped type definition:
136-
137- ``` ts
138- // `MappedType` is structurally identical to Record<CipherType, boolean>
139- type MappedType = { [K in CipherType ]: boolean };
140- ```
141-
142- When the compiler examines ` instance ` , it only knows that the type of each of its members is
143- ` CipherType ` . That is, the type of ` instance ` to the compiler is
144- ` { [K in 1 | 2 | 3 | 4 | 5]?: boolean } ` . This doesn't sufficiently overlap with ` MappedType ` , which
145- is looking for ` { [1]: boolean, [2]: boolean, [3]: boolean, [4]: boolean, [5]: boolean } ` . The
146- failure occurs, because the inferred type can have fewer fields than ` MappedType ` .
147-
148- ### Workarounds
149-
150- ** Option A: Assert the type is correct.** You need to manually verify this. The compiler cannot
151- typecheck it.
152-
153- ``` ts
154- const instance: MappedType = {
155- [CipherType .Login ]: true ,
156- // ...
157- } as MappedType ;
158- ```
159-
160- ** Option B: Define the mapped type as a partial.** Then, inspect its properties before using them.
161-
162- ``` ts
163- type MappedType = { [K in CipherType ]? : boolean };
164- const instance: MappedType = {
165- [CipherType .Login ]: true ,
166- // ...
167- };
168-
169- if (CipherType .Login in instance ) {
170- // work with `instance[CipherType.Login]`
171- }
172- ```
173-
174- ** Option C: Use a collection.** Consider this approach when downstream code reflects over the result
175- with ` in ` or using methods like ` Object.keys ` .
176-
177- ``` ts
178- const collection = new Map ([[CipherType .Login , true ]]);
179-
180- const instance = collection .get (CipherType .Login );
181- if (instance ) {
182- // work with `instance`
183- }
184-
185- const available = [CipherType .Login , CipherType .Card ];
186- if (available .includes (CipherType .Login )) {
187- // ...
188- }
189- ```
190-
191- [ computed-property-names] :
192- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#computed_property_names
193- [ literal-type ] : https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types
194104[ no-enum-lint ] : https://github.com/bitwarden/clients/blob/main/libs/eslint/platform/no-enums.mjs
195105[ no-enum-configuration] :
196106 https://github.com/bitwarden/clients/blob/032fedf308ec251f17632d7d08c4daf6f41a4b1d/eslint.config.mjs#L77
197- [ no-member-fields-fixed] :
198- https://devblogs.microsoft.com/typescript/announcing-typescript-5-8-beta/#preserved-computed-property-names-in-declaration-files
0 commit comments