Skip to content

Add -enumStyle option for erasable enum code generation#56

Merged
corbanbrook merged 12 commits into
masterfrom
enum-to-union-types
Jun 23, 2026
Merged

Add -enumStyle option for erasable enum code generation#56
corbanbrook merged 12 commits into
masterfrom
enum-to-union-types

Conversation

@corbanbrook

@corbanbrook corbanbrook commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a configurable -enumStyle generator option so users can choose how webrpc schema
enums (and the generated error enums) are code-generated in TypeScript:

-enumStyle value Output
enum (default) Traditional TypeScript export enum — unchanged from current behavior
union as const object + union type (erasable, no runtime enum)

The option defaults to enum, so existing output is unchanged unless -enumStyle=union
is explicitly passed.

Motivation

TypeScript best practices are moving away from enum declarations. They emit runtime
code that cannot be erased, which is incompatible with the erasableSyntaxOnly
tsconfig option and with tooling (Node's type stripping, modern bundlers) that expects
type-only syntax to disappear at build time. The union style emits a plain const
object plus a derived union type, leaving no non-erasable runtime construct behind.

Output comparison

Given this schema:

enum Kind: uint32
  - USER
  - ADMIN

enum Intent: string
  - openSession
  - closeSession

-enumStyle=enum (default)

export enum Kind {
  USER = 'USER',
  ADMIN = 'ADMIN'
}

-enumStyle=union

export const Kind = {
  USER: 'USER',
  ADMIN: 'ADMIN',
} as const
export type Kind = (typeof Kind)[keyof typeof Kind]

In union mode the same treatment is applied to the generated errors and
WebrpcErrorCodes enums, so the entire generated file is free of runtime enum
declarations and passes tsc --erasableSyntaxOnly.

Caveats

The union style is mostly a drop-in replacement for an enum, with two behavioral
differences for consumer code:

  • No reverse mapping for numeric enums. WebrpcErrorCodes[1000] returns undefined
    (a numeric enum would return 'Unauthorized'). Code mapping an error code back to its
    name must be updated. Schema enums are unaffected (they use string values).
  • Members can't be used directly as types. type T = Kind.USER becomes
    type T = typeof Kind.USER.

Changes

File Change
main.go.tmpl Add -enumStyle option defaulting to enum; validate value is enum or union, otherwise print an error and exit
types.go.tmpl Branch on $opts.enumStyle — existing export enum block for enum, new as const + union block for union
errors.go.tmpl Apply the same union treatment to the errors and WebrpcErrorCodes enums so union output is fully erasable
README.md Document the option, the output comparison, and the caveats above
tests-unit/ New enum.ridl schema + golden fixtures (enum-default.gen.ts, enum-union.gen.ts) and a enum-union.test.ts vitest spec
.github/workflows/ci.yml Regenerate the enum fixtures from current templates and run the tests-unit vitest suite on every PR

Testing

tests-unit/enum-union.test.ts verifies:

  • Schema and error enums expose the correct runtime values in union mode
  • The union name works in both value and type position (drop-in usage)
  • The union output contains no runtime export enum, and the default output still does
  • tsc --erasableSyntaxOnly compiles the union fixture clean and rejects the default
    fixture with TS1294

Invalid values are rejected: -enumStyle=foo
-enumStyle="foo" is not supported, must be "enum" or "union".

corbanbrook and others added 4 commits June 18, 2026 15:21
Support generating const objects with union types as an alternative to
TypeScript enums, for compatibility with erasableSyntaxOnly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@corbanbrook corbanbrook force-pushed the enum-to-union-types branch from 4707ece to 061a0da Compare June 18, 2026 19:22
corbanbrook and others added 2 commits June 18, 2026 15:31
Add a dedicated enum schema (enum.ridl) with numeric and string enums
plus errors, generate both `enum` and `union` golden fixtures, and a
vitest spec verifying runtime values, drop-in type usage, and—crucially—
that union mode leaves no runtime `export enum` behind (including the
error enums).

Wire a tests-unit vitest run into CI: it regenerates the enum fixtures
from the current templates before running, so union template regressions
are caught. Scoped to `generate:enum` because the pre-existing bigint
`generate` (service.ridl) is incompatible with webrpc-gen v0.37.4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@AlexanderKolberg AlexanderKolberg left a comment

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.

This is great!
I really like this approach with the as const obj + union

Comment thread tests-unit/package.json Outdated
Comment thread README.md Outdated
corbanbrook and others added 5 commits June 19, 2026 10:22
Clarify the flag name ahead of a future -enumWireFormat option. "Style"
reads as "how the enum is emitted in the generated TypeScript" and avoids
overlap with webrpc's existing notion of an enum *type* (isEnumType) and
the planned wire-format flag.

Values are unchanged (enum default, union). No deprecation alias since the
option is unreleased; the old -enum=... now falls through to the generic
"unsupported target option" error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v0.27.0 and v0.28.0 were tagged without the enum feature; bump the
version column to the next release this branch will ship in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add two tsc-based tests: the union output must compile clean under
--erasableSyntaxOnly, and the default `enum` output must be rejected
with TS1294. This is the authoritative erasability check the text-based
assertions only approximated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The union style is mostly a drop-in replacement, but two real differences
from a TypeScript enum can affect consumers: numeric enums lose reverse
mapping (e.g. WebrpcErrorCodes[code]), and enum members can no longer be
used directly as types (type T = Kind.USER -> typeof Kind.USER).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@corbanbrook corbanbrook changed the title Enum union types Add -enumStyle option for erasable enum code generation Jun 19, 2026

@AlexanderKolberg AlexanderKolberg left a comment

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.

LGTM!

@corbanbrook corbanbrook merged commit 60afd54 into master Jun 23, 2026
1 check passed
@corbanbrook corbanbrook deleted the enum-to-union-types branch June 23, 2026 15:43
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.

2 participants