diff --git a/docs/superpowers/specs/2026-03-25-enum-union-types-design.md b/docs/superpowers/specs/2026-03-25-enum-union-types-design.md new file mode 100644 index 0000000..9c49e29 --- /dev/null +++ b/docs/superpowers/specs/2026-03-25-enum-union-types-design.md @@ -0,0 +1,78 @@ +# Configurable Enum Style: enum vs union/const + +## Context + +TypeScript best practices are moving away from `enum` declarations. The `erasableSyntaxOnly` tsconfig option reflects this trend. This change adds a `-enum` generator option so users can choose between traditional TypeScript enums and the modern `as const` object + union type pattern. + +## Option + +**`-enum`** accepts two values: + +| Value | Description | +|-------|-------------| +| `enum` | (default) Current behavior — generates `export enum` | +| `union` | Generates `as const` object + union type | + +## Output Formats + +### Input RIDL + +```ridl +enum Kind: uint32 + - USER + - ADMIN + +enum Intent: string + - openSession + - closeSession +``` + +### `-enum=enum` (default) + +```typescript +export enum Kind { + USER = 'USER', + ADMIN = 'ADMIN' +} + +export enum Intent { + openSession = 'openSession', + closeSession = 'closeSession' +} +``` + +### `-enum=union` + +```typescript +export const Kind = { + USER: 'USER', + ADMIN: 'ADMIN', +} as const +export type Kind = (typeof Kind)[keyof typeof Kind] + +export const Intent = { + openSession: 'openSession', + closeSession: 'closeSession', +} as const +export type Intent = (typeof Intent)[keyof typeof Intent] +``` + +## Design Details + +- The const object and type share the same name — this is intentional and idiomatic TypeScript (declaration merging between value and type namespaces). +- Value logic is the same for both styles: `$field.Value` for string-typed enums, `$field.Name` for others. +- The union type is fully compatible everywhere a TypeScript enum would be used — function parameters, interface fields, etc. — because the type name is identical. +- No changes needed to type references in generated interfaces or client/server code. + +## Files Modified + +1. **`main.go.tmpl`** — Add `enum` option with default `"enum"`, validate accepts `"enum"` or `"union"` +2. **`types.go.tmpl`** — Branch on `$opts.enum`: existing block for `"enum"`, new const+union block for `"union"` +3. **`_examples/`** — Update generated examples + +## Verification + +1. Generate with `-enum=enum` — output should match current behavior exactly +2. Generate with `-enum=union` — output should use `as const` + union type pattern +3. Invalid `-enum=foo` — should print error and exit +4. No `-enum` flag — should default to `"enum"` style diff --git a/main.go.tmpl b/main.go.tmpl index d71c947..644c25c 100644 --- a/main.go.tmpl +++ b/main.go.tmpl @@ -6,6 +6,7 @@ {{- set $opts "server" (ternary (in .Opts.server "" "true") true false) -}} {{- set $opts "compat" (ternary (in .Opts.compat "" "true") true false) -}} {{- set $opts "webrpcHeader" (ternary (eq (default .Opts.webrpcHeader "true") "false") false true) -}} +{{- set $opts "enum" (default .Opts.enum "enum") -}} {{- /* Print help on -help. */ -}} {{- if exists .Opts "help" -}} @@ -22,6 +23,11 @@ {{- end -}} {{- end -}} +{{- if not (in $opts.enum "enum" "union") -}} + {{- stderrPrintf "-enum=%q is not supported, must be \"enum\" or \"union\"\n" $opts.enum -}} + {{- exit 1 -}} +{{- end -}} + {{- if ne .WebrpcVersion "v1" -}} {{- stderrPrintf "%s generator error: unsupported Webrpc version %s\n" .WebrpcTarget .WebrpcVersion -}} {{- exit 1 -}} diff --git a/types.go.tmpl b/types.go.tmpl index aeb3a53..22f391f 100644 --- a/types.go.tmpl +++ b/types.go.tmpl @@ -12,6 +12,18 @@ {{range $_i, $type := $types -}} {{if isEnumType $type }} +{{- if eq $opts.enum "union"}} +export const {{$type.Name}} = { +{{- range $i, $field := $type.Fields}} + {{- if eq $type.Type.Expr "string"}} + {{$field.Name}}: '{{$field.Value}}', + {{- else}} + {{$field.Name}}: '{{$field.Name}}', + {{- end}} +{{- end}} +} as const +export type {{$type.Name}} = (typeof {{$type.Name}})[keyof typeof {{$type.Name}}] +{{- else}} export enum {{$type.Name}} { {{- range $i, $field := $type.Fields}} {{- if $i}},{{end}} @@ -22,6 +34,7 @@ export enum {{$type.Name}} { {{- end}} {{- end}} } +{{- end}} {{end -}} {{- if isStructType $type }}