Skip to content

Commit 4032e1a

Browse files
authored
Merge pull request #91 from PY44N/main
Add Support for Typescript Enum & Union Generation
2 parents b277146 + 10d4c17 commit 4032e1a

File tree

8 files changed

+735
-24
lines changed

8 files changed

+735
-24
lines changed

README.md

Lines changed: 121 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ type ListUsersResponse struct {
5353
}
5454
```
5555

56-
_Typescript output_
56+
_Typescript output (with default `enum_style: "const"`)_
5757

5858
```typescript
5959
/**
@@ -141,7 +141,7 @@ err := gen.Generate()
141141
# You can specify default mappings that will apply to all packages.
142142
type_mappings:
143143
time.Time: "string /* RFC3339 */"
144-
144+
145145
# You can specify more than one package
146146
packages:
147147
# The package path just like you would import it in Go
@@ -173,6 +173,12 @@ packages:
173173
# Package that the generates Typescript types should extend. This is useful when
174174
# attaching your types to a generic ORM.
175175
extends: "SomeType"
176+
177+
# Enum generation style. Supported values: "const" (default), "enum", "union".
178+
# "const" generates individual export const declarations (traditional behavior).
179+
# "enum" generates TypeScript enum declarations for Go const groups.
180+
# "union" generates TypeScript union type declarations for Go const groups.
181+
enum_style: "enum"
176182
```
177183
178184
See also the source file [tygo/config.go](./tygo/config.go).
@@ -207,10 +213,11 @@ You could use the `frontmatter` field in the config to inject `export type Genre
207213

208214
**`tygo:emit` directive**
209215

210-
Another way to generate types that cannot be directly represented in Go is to use a `//tygo:emit` directive to
216+
Another way to generate types that cannot be directly represented in Go is to use a `//tygo:emit` directive to
211217
directly emit literal TS code.
212-
The directive can be used in two ways. A `tygo:emit` directive on a struct will emit the remainder of the directive
218+
The directive can be used in two ways. A `tygo:emit` directive on a struct will emit the remainder of the directive
213219
text before the struct.
220+
214221
```golang
215222
// Golang input
216223

@@ -222,7 +229,7 @@ type Book struct {
222229
```
223230

224231
```typescript
225-
export type Genre = "novel" | "crime" | "fantasy"
232+
export type Genre = "novel" | "crime" | "fantasy";
226233

227234
export interface Book {
228235
title: string;
@@ -231,11 +238,12 @@ export interface Book {
231238
```
232239

233240
A `//tygo:emit` directive on a string var will emit the contents of the var, useful for multi-line content.
241+
234242
```golang
235243
//tygo:emit
236244
var _ = `export type StructAsTuple=[
237-
a:number,
238-
b:number,
245+
a:number,
246+
b:number,
239247
c:string,
240248
]
241249
`
@@ -245,16 +253,11 @@ type CustomMarshalled struct {
245253
```
246254

247255
```typescript
248-
export type StructAsTuple=[
249-
a:number,
250-
b:number,
251-
c:string,
252-
]
256+
export type StructAsTuple = [a: number, b: number, c: string];
253257

254258
export interface CustomMarshalled {
255259
content: StructAsTuple[];
256260
}
257-
258261
```
259262

260263
Generating types this way is particularly useful for tuple types, because a comma cannot be used in the `tstype` tag.
@@ -396,6 +399,111 @@ export interface ABCD<
396399
}
397400
```
398401

402+
## TypeScript Enum and Union Generation
403+
404+
Tygo can generate native TypeScript enums or union types from Go const groups. When `enum_style: "enum"` is configured, tygo detects Go constant groups that follow enum-like patterns and converts them to TypeScript enums. When `enum_style: "union"` is configured, the same const groups are converted to TypeScript union types instead.
405+
406+
### Requirements for Enum/Union Generation
407+
408+
For a const group to be recognized as an enum or union:
409+
410+
1. It must contain at least 2 exported constants
411+
2. All constants should share the same type (e.g., `UserRole`)
412+
3. Constant names should follow a consistent prefix pattern (e.g., `UserRoleDefault`, `UserRoleEditor`)
413+
414+
### Examples
415+
416+
**String Enums:**
417+
418+
```go
419+
// Go input
420+
type Status = string
421+
const (
422+
StatusActive Status = "active"
423+
StatusInactive Status = "inactive"
424+
StatusPending Status = "pending"
425+
)
426+
```
427+
428+
```typescript
429+
// TypeScript output (with enum_style: "enum")
430+
export enum Status {
431+
Active = "active",
432+
Inactive = "inactive",
433+
Pending = "pending",
434+
}
435+
```
436+
437+
```typescript
438+
// TypeScript output (with enum_style: "union")
439+
export const StatusActive = "active";
440+
export const StatusInactive = "inactive";
441+
export const StatusPending = "pending";
442+
export type Status = typeof StatusActive | typeof StatusInactive | typeof StatusPending;
443+
```
444+
445+
**Numeric Enums with iota:**
446+
447+
```go
448+
// Go input
449+
type Priority int
450+
const (
451+
PriorityLow Priority = iota
452+
PriorityMedium
453+
PriorityHigh
454+
)
455+
```
456+
457+
```typescript
458+
// TypeScript output (with enum_style: "enum")
459+
export enum Priority {
460+
Low = 0,
461+
Medium,
462+
High,
463+
}
464+
```
465+
466+
```typescript
467+
// TypeScript output (with enum_style: "union")
468+
export const PriorityLow = 0;
469+
export const PriorityMedium = 1;
470+
export const PriorityHigh = 2;
471+
export type Priority = typeof PriorityLow | typeof PriorityMedium | typeof PriorityHigh;
472+
```
473+
474+
**Mixed Const Blocks:**
475+
When a const block contains both enum-like constants and other constants, tygo generates an enum for the matching constants and individual const declarations for the rest:
476+
477+
```go
478+
// Go input
479+
type UserRole = string
480+
const (
481+
UserRoleAdmin UserRole = "admin"
482+
UserRoleGuest UserRole = "guest"
483+
MaxRetries = 5
484+
DefaultTimeout = 30
485+
)
486+
```
487+
488+
```typescript
489+
// TypeScript output (with enum_style: "enum")
490+
export enum UserRole {
491+
Admin = "admin",
492+
Guest = "guest",
493+
}
494+
export const MaxRetries = 5;
495+
export const DefaultTimeout = 30;
496+
```
497+
498+
```typescript
499+
// TypeScript output (with enum_style: "union")
500+
export const UserRoleAdmin = "admin";
501+
export const UserRoleGuest = "guest";
502+
export type UserRole = typeof UserRoleAdmin | typeof UserRoleGuest;
503+
export const MaxRetries = 5;
504+
export const DefaultTimeout = 30;
505+
```
506+
399507
## YAML support
400508

401509
Tygo supports generating typings for YAML-serializable objects that can be understood by Go apps.

tygo.yaml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,8 @@ packages:
5656
- path: "github.com/gzuidhof/tygo/examples/interface"
5757
- path: "github.com/gzuidhof/tygo/examples/directive"
5858
- path: "github.com/gzuidhof/tygo/examples/emit"
59-
exclude_files:
59+
exclude_files:
6060
- "excluded.go"
6161
- path: "github.com/gzuidhof/tygo/examples/rune"
6262

6363
- path: "github.com/gzuidhof/tygo/examples/globalconfig"
64-
65-

tygo/config.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
const defaultOutputFilename = "index.ts"
1111
const defaultFallbackType = "any"
1212
const defaultPreserveComments = "default"
13+
const defaultEnumStyle = "const"
1314

1415
type PackageConfig struct {
1516
// The package path just like you would import it in Go
@@ -61,6 +62,13 @@ type PackageConfig struct {
6162
// Default is "undefined".
6263
// Useful for usage with JSON marshalers that output null for optional fields (e.g. gofiber JSON).
6364
OptionalType string `yaml:"optional_type"`
65+
66+
// Set the enum generation style.
67+
// Supported values: "const" (default), "enum", "union".
68+
// "const" generates individual export const declarations (current behavior).
69+
// "enum" generates TypeScript enum declarations.
70+
// "union" generates TypeScript union type declarations.
71+
EnumStyle string `yaml:"enum_style"`
6472
}
6573

6674
type Config struct {
@@ -128,6 +136,19 @@ func normalizeOptionalType(optional string) (string, error) {
128136
}
129137
}
130138

139+
func normalizeEnumStyle(enumStyle string) (string, error) {
140+
switch enumStyle {
141+
case "", "const":
142+
return "const", nil
143+
case "enum":
144+
return "enum", nil
145+
case "union":
146+
return "union", nil
147+
default:
148+
return "", fmt.Errorf("unsupported enum_style: %s", enumStyle)
149+
}
150+
}
151+
131152
func (c PackageConfig) IsFileIgnored(pathToFile string) bool {
132153
basename := filepath.Base(pathToFile)
133154
for _, ef := range c.ExcludeFiles {
@@ -172,6 +193,10 @@ func (pc PackageConfig) Normalize() (PackageConfig, error) {
172193
pc.PreserveComments = defaultPreserveComments
173194
}
174195

196+
if pc.EnumStyle == "" {
197+
pc.EnumStyle = defaultEnumStyle
198+
}
199+
175200
var err error
176201
pc.Flavor, err = normalizeFlavor(pc.Flavor)
177202
if err != nil {
@@ -188,6 +213,11 @@ func (pc PackageConfig) Normalize() (PackageConfig, error) {
188213
return pc, fmt.Errorf("invalid optional_type config for package %s: %s", pc.Path, err)
189214
}
190215

216+
pc.EnumStyle, err = normalizeEnumStyle(pc.EnumStyle)
217+
if err != nil {
218+
return pc, fmt.Errorf("invalid enum_style config for package %s: %s", pc.Path, err)
219+
}
220+
191221
return pc, nil
192222
}
193223

tygo/convert_string.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ func ConvertGoToTypescript(goCode string, pkgConfig PackageConfig) (string, erro
2828
}
2929

3030
pkgGen := &PackageGenerator{
31-
conf: &pkgConfig,
32-
pkg: nil,
31+
conf: &pkgConfig,
32+
pkg: nil,
33+
generatedEnums: make(map[string]bool),
3334
}
3435

3536
s := new(strings.Builder)

tygo/generator.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ type Tygo struct {
1818

1919
// Responsible for generating the code for an input package
2020
type PackageGenerator struct {
21-
conf *PackageConfig
22-
pkg *packages.Package
23-
GoFiles []string
21+
conf *PackageConfig
22+
pkg *packages.Package
23+
GoFiles []string
24+
generatedEnums map[string]bool // Track types that have been generated as enums
2425
}
2526

2627
func New(config *Config) *Tygo {
@@ -56,9 +57,10 @@ func (g *Tygo) Generate() error {
5657
pkgConfig := g.conf.PackageConfig(pkg.ID)
5758

5859
pkgGen := &PackageGenerator{
59-
conf: pkgConfig,
60-
GoFiles: pkg.GoFiles,
61-
pkg: pkg,
60+
conf: pkgConfig,
61+
GoFiles: pkg.GoFiles,
62+
pkg: pkg,
63+
generatedEnums: make(map[string]bool),
6264
}
6365
g.packageGenerators[pkg.PkgPath] = pkgGen
6466
code, err := pkgGen.Generate()

tygo/package_generator.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,24 @@ import (
66
"strings"
77
)
88

9+
// preProcessEnums scans the file for const declarations that will be converted to enums
10+
// and marks the corresponding types to prevent duplicate type declarations
11+
func (g *PackageGenerator) preProcessEnums(file *ast.File) {
12+
ast.Inspect(file, func(n ast.Node) bool {
13+
if decl, ok := n.(*ast.GenDecl); ok && decl.Tok == token.CONST {
14+
if enumGroup := g.detectEnumGroup(decl); enumGroup != nil {
15+
g.generatedEnums[enumGroup.typeName] = true
16+
}
17+
}
18+
return true
19+
})
20+
}
21+
922
// generateFile writes the generated code for a single file to the given strings.Builder.
1023
func (g *PackageGenerator) generateFile(s *strings.Builder, file *ast.File, filepath string) {
24+
// First pass: identify types that will be generated as enums
25+
g.preProcessEnums(file)
26+
1127
first := true
1228

1329
ast.Inspect(file, func(n ast.Node) bool {

0 commit comments

Comments
 (0)