Thanks to visit codestin.com
Credit goes to github.com

Skip to content

!feat: use first class typescript enums #20

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 3 commits into from
Apr 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,32 @@ Using [goja](https://github.com/dop251/goja), these types are then serialized to

# Generator Opinions

The generator aims to do the bare minimum type conversion. An example of a common opinion, is to create enum lists.
The generator aims to do the bare minimum type conversion. An example of a common opinion, is to use types to represent enums. Without the mutation, the following is generated:

```typescript
export type Enum = "bar" | "baz" | "foo" | "qux" // <-- Golang type
export const Enums: Enum[] = ["bar", "baz", "foo", "qux"] // <-- Helpful additional generated type
export enum EnumString {
EnumBar = "bar",
EnumBaz = "baz",
EnumFoo = "foo",
EnumQux = "qux"
}
```

These kinds of opinions can be added with:
Add the mutation:
```golang
ts.ApplyMutations(
config.EnumLists,
config.EnumAsTypes,
)
output, _ := ts.Serialize()
```

And the output is:

```typescript
export type EnumString = "bar" | "baz" | "foo" | "qux";
```


# Helpful notes

An incredible website to visualize the AST of typescript: https://ts-ast-viewer.com/
57 changes: 57 additions & 0 deletions bindings/bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ func (b *Bindings) ToTypescriptDeclarationNode(ety DeclarationType) (*goja.Objec
siObj, err = b.Alias(ety)
case *VariableStatement:
siObj, err = b.VariableStatement(ety)
case *Enum:
siObj, err = b.EnumDeclaration(ety)
default:
return nil, xerrors.Errorf("unsupported type for declaration type: %T", ety)
}
Expand All @@ -72,6 +74,8 @@ func (b *Bindings) ToTypescriptExpressionNode(ety ExpressionType) (*goja.Object,
siObj, err = b.Array(ety.Node)
case *UnionType:
siObj, err = b.Union(ety)
case *EnumMember:
siObj, err = b.EnumMember(ety)
case *Null:
siObj, err = b.Null()
case *VariableDeclarationList:
Expand Down Expand Up @@ -646,3 +650,56 @@ func (b *Bindings) OperatorNode(value *OperatorNodeType) (*goja.Object, error) {
}
return res.ToObject(b.vm), nil
}

func (b *Bindings) EnumMember(value *EnumMember) (*goja.Object, error) {
literalF, err := b.f("enumMember")
if err != nil {
return nil, err
}

obj := goja.Undefined()
if value.Value != nil {
obj, err = b.ToTypescriptExpressionNode(value.Value)
if err != nil {
return nil, fmt.Errorf("enum member type: %w", err)
}
}

res, err := literalF(goja.Undefined(), b.vm.ToValue(value.Name), obj)
if err != nil {
return nil, xerrors.Errorf("call enumMember: %w", err)
}
return res.ToObject(b.vm), nil
}

func (b *Bindings) EnumDeclaration(e *Enum) (*goja.Object, error) {
aliasFunc, err := b.f("enumDeclaration")
if err != nil {
return nil, err
}

var members []any
for _, m := range e.Members {
v, err := b.ToTypescriptExpressionNode(m)
if err != nil {
return nil, fmt.Errorf("enum type: %w", err)
}
members = append(members, v)
}

res, err := aliasFunc(goja.Undefined(),
b.vm.ToValue(ToStrings(e.Modifiers)),
b.vm.ToValue(e.Name.Ref()),
b.vm.NewArray(members...),
)
if err != nil {
return nil, xerrors.Errorf("call enumDeclaration: %w", err)
}

obj := res.ToObject(b.vm)
if e.Source.File != "" {
return b.Comment(e.Source.Comment(obj))
}

return obj, nil
}
10 changes: 10 additions & 0 deletions bindings/declarations.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,13 @@ type VariableStatement struct {

func (*VariableStatement) isNode() {}
func (*VariableStatement) isDeclarationType() {}

type Enum struct {
Name Identifier
Modifiers []Modifier
Members []*EnumMember
Source
}

func (*Enum) isNode() {}
func (*Enum) isDeclarationType() {}
9 changes: 9 additions & 0 deletions bindings/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,12 @@ func OperatorNode(keyword LiteralKeyword, node ExpressionType) *OperatorNodeType

func (*OperatorNodeType) isNode() {}
func (*OperatorNodeType) isExpressionType() {}

type EnumMember struct {
Name string
// Value is allowed to be nil, which results in `undefined`.
Value ExpressionType
}

func (*EnumMember) isNode() {}
func (*EnumMember) isExpressionType() {}
4 changes: 4 additions & 0 deletions bindings/walk/walk.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ func Walk(v Visitor, node bindings.Node) {
Walk(v, n.Type)
case *bindings.UnionType:
walkList(v, n.Types)
case *bindings.Enum:
walkList(v, n.Members)
case *bindings.VariableStatement:
Walk(v, n.Declarations)
case *bindings.VariableDeclarationList:
Expand All @@ -62,6 +64,8 @@ func Walk(v Visitor, node bindings.Node) {
walkList(v, n.Args)
case *bindings.OperatorNodeType:
Walk(v, n.Type)
case *bindings.EnumMember:
Walk(v, n.Value)
default:
panic(fmt.Sprintf("convert.Walk: unexpected node type %T", n))
}
Expand Down
174 changes: 118 additions & 56 deletions config/mutations.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ func ExportTypes(ts *guts.Typescript) {
node.Modifiers = append(node.Modifiers, bindings.ModifierExport)
case *bindings.VariableStatement:
node.Modifiers = append(node.Modifiers, bindings.ModifierExport)
case *bindings.Enum:
node.Modifiers = append(node.Modifiers, bindings.ModifierExport)
default:
panic(fmt.Sprintf("unexpected node type %T for exporting", node))
}
Expand All @@ -72,12 +74,56 @@ func ReadOnly(ts *guts.Typescript) {
}
}
case *bindings.VariableStatement:
case *bindings.Enum:
// Enums are immutable by default
default:
panic("unexpected node type for exporting")
}
})
}

// TrimEnumPrefix removes the enum name from the member names.
func TrimEnumPrefix(ts *guts.Typescript) {
ts.ForEach(func(key string, node bindings.Node) {
enum, ok := node.(*bindings.Enum)
if !ok {
return
}

for _, member := range enum.Members {
member.Name = strings.TrimPrefix(member.Name, enum.Name.Name)
}
})
}

// EnumAsTypes uses types to handle enums rather than using 'enum'.
// An enum will look like:
// type EnumString = "bar" | "baz" | "foo" | "qux";
func EnumAsTypes(ts *guts.Typescript) {
ts.ForEach(func(key string, node bindings.Node) {
enum, ok := node.(*bindings.Enum)
if !ok {
return
}

// Convert the enum to a union type
union := &bindings.UnionType{
Types: make([]bindings.ExpressionType, 0, len(enum.Members)),
}
for _, member := range enum.Members {
union.Types = append(union.Types, member.Value)
}

// Replace the enum with an alias type
ts.ReplaceNode(key, &bindings.Alias{
Name: enum.Name,
Modifiers: enum.Modifiers,
Type: union,
Source: enum.Source,
})
})
}

// EnumLists adds a constant that lists all the values in a given enum.
// Example:
// type MyEnum = string
Expand All @@ -86,72 +132,53 @@ func ReadOnly(ts *guts.Typescript) {
// EnumBar = "bar"
// )
// const MyEnums: string = ["foo", "bar"] <-- this is added
// TODO: Enums were changed to use proper enum types. This should be
// updated to support that. EnumLists only works with EnumAsTypes used first.
func EnumLists(ts *guts.Typescript) {
addNodes := make(map[string]bindings.Node)
ts.ForEach(func(key string, node bindings.Node) {
switch node := node.(type) {
// Find the enums, and make a list of values.
// Only support primitive types.
case *bindings.Alias:
if union, ok := node.Type.(*bindings.UnionType); ok {
if len(union.Types) == 0 {
return
}

var expectedType *bindings.LiteralType
// This might be a union type, if all elements are the same literal type.
for _, t := range union.Types {
value, ok := t.(*bindings.LiteralType)
if !ok {
return
}
if expectedType == nil {
expectedType = value
continue
}

if reflect.TypeOf(expectedType.Value) != reflect.TypeOf(value.Value) {
return
}
}
_, union, ok := isGoEnum(node)
if !ok {
return
}

values := make([]bindings.ExpressionType, 0, len(union.Types))
for _, t := range union.Types {
values = append(values, t)
}
values := make([]bindings.ExpressionType, 0, len(union.Types))
for _, t := range union.Types {
values = append(values, t)
}

// Pluralize the name
name := key + "s"
switch key[len(key)-1] {
case 'x', 's', 'z':
name = key + "es"
}
if strings.HasSuffix(key, "ch") || strings.HasSuffix(key, "sh") {
name = key + "es"
}
// Pluralize the name
name := key + "s"
switch key[len(key)-1] {
case 'x', 's', 'z':
name = key + "es"
}
if strings.HasSuffix(key, "ch") || strings.HasSuffix(key, "sh") {
name = key + "es"
}

addNodes[name] = &bindings.VariableStatement{
Modifiers: []bindings.Modifier{},
Declarations: &bindings.VariableDeclarationList{
Declarations: []*bindings.VariableDeclaration{
{
// TODO: Fix this with Identifier's instead of "string"
Name: bindings.Identifier{Name: name},
ExclamationMark: false,
Type: &bindings.ArrayType{
// The type is the enum type
Node: bindings.Reference(bindings.Identifier{Name: key}),
},
Initializer: &bindings.ArrayLiteralType{
Elements: values,
},
},
addNodes[name] = &bindings.VariableStatement{
Modifiers: []bindings.Modifier{},
Declarations: &bindings.VariableDeclarationList{
Declarations: []*bindings.VariableDeclaration{
{
// TODO: Fix this with Identifier's instead of "string"
Name: bindings.Identifier{Name: name},
ExclamationMark: false,
Type: &bindings.ArrayType{
// The type is the enum type
Node: bindings.Reference(bindings.Identifier{Name: key}),
},
Initializer: &bindings.ArrayLiteralType{
Elements: values,
},
Flags: bindings.NodeFlagsConstant,
},
Source: bindings.Source{},
}
}
},
Flags: bindings.NodeFlagsConstant,
},
Source: bindings.Source{},
}
})

Expand Down Expand Up @@ -305,3 +332,38 @@ func (v *notNullMaps) Visit(node bindings.Node) walk.Visitor {

return v
}

func isGoEnum(n bindings.Node) (*bindings.Alias, *bindings.UnionType, bool) {
al, ok := n.(*bindings.Alias)
if !ok {
return nil, nil, false
}

union, ok := al.Type.(*bindings.UnionType)
if !ok {
return nil, nil, false
}

if len(union.Types) == 0 {
return nil, nil, false
}

var expectedType *bindings.LiteralType
// This might be a union type, if all elements are the same literal type.
for _, t := range union.Types {
value, ok := t.(*bindings.LiteralType)
if !ok {
return nil, nil, false
}
if expectedType == nil {
expectedType = value
continue
}

if reflect.TypeOf(expectedType.Value) != reflect.TypeOf(value.Value) {
return nil, nil, false
}
}

return al, union, true
}
5 changes: 4 additions & 1 deletion convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,7 +529,10 @@ func (ts *Typescript) parse(obj types.Object) error {
// type. However, the order types are parsed is not guaranteed, so we
// add the enum to the Alias as a post-processing step.
ts.updateNode(enumObjName.Ref(), func(n *typescriptNode) {
n.AddEnum(constValue)
n.AddEnum(&bindings.EnumMember{
Name: obj.Name(),
Value: constValue,
})
})
return nil
case *types.Func:
Expand Down
Loading