Skip to content

Introduce ADR-0025: deprecate typescript enums #605

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jun 4, 2025

Conversation

audreyality
Copy link
Member

@audreyality audreyality commented May 30, 2025

๐ŸŽŸ๏ธ Tracking

N/A

๐Ÿ“” Objective

Document decision to deprecate typescript enums across the bitwarden/clients codebase.

(Guidelines - rendered, ADR - rendered)

๐Ÿฆฎ Reviewer guidelines

  • ๐Ÿ‘ (:+1:) or similar for great changes
  • ๐Ÿ“ (:memo:) or โ„น๏ธ (:information_source:) for notes or general info
  • โ“ (:question:) for questions
  • ๐Ÿค” (:thinking:) or ๐Ÿ’ญ (:thought_balloon:) for more open inquiry that's not quite a confirmed
    issue and could potentially benefit from discussion
  • ๐ŸŽจ (:art:) for suggestions / improvements
  • โŒ (:x:) or โš ๏ธ (:warning:) for more significant problems or concerns needing attention
  • ๐ŸŒฑ (:seedling:) or โ™ป๏ธ (:recycle:) for future improvements or indications of technical debt
  • โ› (:pick:) for minor or nitpick changes

@github-actions github-actions bot added the adr label May 30, 2025
@audreyality audreyality requested review from Patrick-Pimentel-Bitwarden and removed request for withinfocus May 30, 2025 16:42
@audreyality

This comment was marked as outdated.

This comment was marked as off-topic.

Copy link

cloudflare-workers-and-pages bot commented May 30, 2025

Deploying contributing-docs with ย Cloudflare Pages ย Cloudflare Pages

Latest commit: 58e5177
Status:ย โœ…ย  Deploy successful!
Preview URL: https://e1b5014a.contributing-docs.pages.dev
Branch Preview URL: https://deprecate-ts-enums-adr.contributing-docs.pages.dev

View logs

Copy link
Member Author

@audreyality audreyality May 30, 2025

Choose a reason for hiding this comment

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

๐Ÿ“ Our discussions to date have indicated that our Rust code can still generate enums, so this document assumes that we can deprecate but not eliminate enums.

๐Ÿ’ญ I'm not sure if we should include direction for the SDK in this. Per @dani-garcia rust-bindgen produces typescript enums and we can override this with tsify. The "deprecate" option includes an exception made for this use-case. If we want to deprecate these uses too, then I think we'd want to go with the "eliminate" option and expand the scope of the ADR to include the rust/SDK changes.

Copy link
Member

Choose a reason for hiding this comment

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

So I went to look a bit more into what wasm-bindgen is doing internally when not using tsify, and this is what it generates:

In *.d.ts file:

export enum LogLevel {
  Trace = 0,
   ... all other variants
}

In the *.js file:

/**
 * @enum {0 | 1 | 2 | 3 | 4}
 */
export const LogLevel = Object.freeze({
  Trace: 0,
  0: "Trace",
  ... all other variants
});

Not sure why they have that mapping from the number back to the string value, but other than that this seems fairly similar to what we want (but without the type alias).

I still think we should be encouraging using full discriminated unions with tsify, though.

Copy link
Member Author

@audreyality audreyality Jun 2, 2025

Choose a reason for hiding this comment

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

Not sure why they have that mapping from the number back to the string value, but other than that this seems fairly similar to what we want (but without the type alias).

It's maintaining compatibility with TypeScript. TS enums support bidirectional mapping. You can use the symbol name to get the value or the value to get the symbol name.

The pattern that @Patrick-Pimentel-Bitwarden, @coroiu, and I have been working on moves the name mapping into a utility function so that we can provide proper error handling. It's been extracted from this PR and moved to #607, since the ADR has to go through the architecture council.

coroiu added a commit that referenced this pull request Jun 2, 2025
Co-authored-by: โœจ Audrey โœจ <ajensen@bitwarden.com>
coroiu added a commit that referenced this pull request Jun 2, 2025
* feat: recommend same type and value names

Use the same name for the type and the value allows us to emulate regular Enums much better.

* feat: merge with @audreyality version from #605

Co-authored-by: โœจ Audrey โœจ <ajensen@bitwarden.com>

* fix: remove broken link

* fix: change name -> value

Co-authored-by: โœจ Audrey โœจ <ajensen@bitwarden.com>

---------

Co-authored-by: โœจ Audrey โœจ <ajensen@bitwarden.com>
@audreyality audreyality requested review from blackwood and removed request for Patrick-Pimentel-Bitwarden, MGibson1 and jprusik June 3, 2025 17:05
@audreyality

This comment was marked as outdated.

Copy link
Contributor

@withinfocus withinfocus left a comment

Choose a reason for hiding this comment

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

I feel this works as a provisional first pass and could be merged as proposed, but up to you if you want to take in more feedback. Said feedback can also come as a result of the council session.

@audreyality audreyality marked this pull request as ready for review June 3, 2025 17:31
@audreyality audreyality requested review from a team as code owners June 3, 2025 17:31
withinfocus
withinfocus previously approved these changes Jun 3, 2025
Copy link
Contributor

@coroiu coroiu left a comment

Choose a reason for hiding this comment

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

Overall this looks really good, just one minor issue and then one thought about our recommended approach

Comment on lines 78 to 90
## Considered Options

- **Allow enums, but advise against them** - This is the current state of affairs. With this option,
teams **must** address the FIXMEs, but _may_ address them by disabling the lint.
- **Deprecate enum use** - Allow enums to exist for historic or technical purposes, but prohibit the
introduction of new ones. Increase the lint to a "warning" and allow the lint to be disabled.
- **Eliminate enum use** - Prohibit the introduction of any new enum and replace all enums in the
codebase with typescript objects. Increase the lint to an "error" and prohibit disabling of the
lint.

## Decision Outcome

Chosen option: **Deprecate enum use**
Copy link
Contributor

Choose a reason for hiding this comment

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

issue?: I think the route we took is between the definitions of "Deprecate" and "Eliminate". We made the lint rule throw errors (not warnings) but we also allowed and did disable those errors for existing cases

Copy link
Member Author

Choose a reason for hiding this comment

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

According to the code, it's presently a suggestion-level lint.

Copy link
Contributor

@coroiu coroiu Jun 4, 2025

Choose a reason for hiding this comment

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

Copy link
Member Author

@audreyality audreyality Jun 4, 2025

Choose a reason for hiding this comment

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

JavaScript is really embracing it's namesake here...

Addressed in 58e5177.

### Plan

- Update contributing docs with patterns and best practices for enum replacement.
- Update the reporting level of the lint to "warning".
Copy link
Contributor

Choose a reason for hiding this comment

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

Or are you maybe suggesting a change to our current approach, i.e. to downgrade error -> warning

Copy link
Member Author

Choose a reason for hiding this comment

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

Addressed in 58e5177.

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
Copy link
Contributor

Choose a reason for hiding this comment

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

thought: I feel like none of these workarounds actually fix the issue which causes less than possible static type-safety. Are we doing this because we value consistency more than the possibility of statically checked mapped types? Should we consider an "escape hatch" that allows access to the literally typed member in our recommended approach?

const instance: Record<CipherType, boolean> = {
  [CipherType[Literal].Login]: true,
  [CipherType[Literal].SecureNote]: false,
  [CipherType[Literal].Card]: true,
  [CipherType[Literal].Identity]: true,
  [CipherType[Literal].SshKey]: true,
};

I guess this is a perfect topic for the arch meeting :)

Copy link
Member Author

@audreyality audreyality Jun 4, 2025

Choose a reason for hiding this comment

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

๐Ÿ‘๐Ÿป Before I respond with my opinions, I totally agree this is why we need the arch meeting.

I feel like none of these workarounds actually fix the issue which causes less than possible static type-safety.

โ›๏ธ That's what makes them a workaround. If they fixed the issue, they'd just be part of the documented solution. ๐Ÿ˜‰

Are we doing this because we value consistency more than the possibility of statically checked mapped types?

๐Ÿค” I'd characterize this differently:

  1. This proposal assumes that correctly inferring the enum type in low-level scenarios is preferable to the compiler inferring the type to be number.
  2. It further assumes that losing type safety when constructing mapped types using literals is an acceptable tradeoff.

And as to why I think this is justified?

๐ŸŽจ The most important aspect to me is that these types produce an awful developer experience. The instantiated type loses the symbols completely, so your debugger will show the object like this:

{ 1: true, 2: false, 3: true, 4: true, 5: true }

In practice it's difficult to get the enum definition in scope, so you just gotta know what symbol goes with each of those numbers.

โ™ป๏ธ I suspect a more compelling case is that the enum-like pattern without the typecast still has type assertions that can't be guaranteed by the compiler, they're just easier to gloss over. In the following code, wrong and invalid infer CipherType even though 8 is invalid due to an incorrect type assertion.

const wrong = [1, 2, 8] as CipherType[];
const invalid = [CipherType.Login, 2, 8] as CipherType[];

Without the type assertions, the statements typecheck as the correct type, number. Critically, an array composed entirely from CipherType typechecks as CipherType!

const numberArray = [1, 2, 8];
const alsoANumberArray = [CipherType.Login, 2, 8];

const cipherTypeArray = [CipherType.Login, CipherType.Card];

This matters for when you go to use the array later. In the following code, typecheckFails causes a type error because number[] cannot be assigned to cipherType. Attempting to assign wrong or invalid passes the typechecker.

// compiler error
const typecheckFails: CipherType[] = numberArray;

// totally fine
const wrongAssignment: CipherType[] = wrong;
const invalidAssignment: CipherType[] = invalid;

// the correct code asserts the proper type using a type predicate
function isCipherType(value: number): value is CipherType {
  return Object.values(_CipherType).includes(value as any);
}

const correct = numberArray.filter(isCipherType);

๐Ÿ“ The kicker is that in the code I've seen from Vault and Tools, the invalid as CipherType code shows up more frequently than types that map over enums.

Important

I didn't do a full review of our code, so I welcome data that proves me wrong here.

Should we consider an "escape hatch" that allows access to the literally typed member in our recommended approach?

I think adding that in causes us to continue repeating bad designs. If we want this capability, we need to admit that enums solve it better than an enum-like ever will.

blackwood
blackwood previously approved these changes Jun 4, 2025
@audreyality audreyality enabled auto-merge (squash) June 4, 2025 15:39
@audreyality audreyality requested a review from withinfocus June 4, 2025 15:39
- 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 increases the number of FIXME comments in the code by about 10%.
- The lint increased the number of FIXME comments in the code by about 10%.
Copy link
Contributor

Choose a reason for hiding this comment

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

โ›๏ธ This would be present tense; don't understand the change.

Copy link
Member Author

@audreyality audreyality Jun 4, 2025

Choose a reason for hiding this comment

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

Andreas pointed out that our current approach configures the lint as an error. When I wrote this, I'd gathered it was only a suggestion. The lint is already active in a more restrictive capacity than this ADR proposes--so strict that the code would not build without the FIXMEs--hence this consequence in the past tense.

Copy link
Contributor

@withinfocus withinfocus left a comment

Choose a reason for hiding this comment

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

I'll leave final acceptance to the engineers working through the code suggestions here.

@audreyality audreyality merged commit 07cd60e into main Jun 4, 2025
10 checks passed
@audreyality audreyality deleted the deprecate-ts-enums-adr branch June 4, 2025 15:45
@withinfocus
Copy link
Contributor

(auto-merge kills me in these situations)

@audreyality
Copy link
Member Author

My bad. I figured the remaining suggestions were open questions for the architecture council. ๐Ÿซ 

@withinfocus
Copy link
Contributor

I just wanted an engineer to give the final blessing. We'll get to any edits at the council session.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants