Skip to content
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
65 changes: 65 additions & 0 deletions bindings/bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ func (b *Bindings) ToTypescriptExpressionNode(ety ExpressionType) (*goja.Object,
siObj, err = b.ArrayLiteral(ety)
case *OperatorNodeType:
siObj, err = b.OperatorNode(ety)
case *TypeLiteralNode:
siObj, err = b.TypeLiteralNode(ety)
case *TypeIntersection:
siObj, err = b.TypeIntersection(ety)
default:
return nil, xerrors.Errorf("unsupported type for field type: %T", ety)
}
Expand Down Expand Up @@ -703,3 +707,64 @@ func (b *Bindings) EnumDeclaration(e *Enum) (*goja.Object, error) {

return obj, nil
}

func (b *Bindings) TypeLiteralNode(node *TypeLiteralNode) (*goja.Object, error) {
typeLiteralF, err := b.f("typeLiteralNode")
if err != nil {
return nil, err
}

var members []interface{}
for _, member := range node.Members {
v, err := b.PropertySignature(member)
if err != nil {
return nil, err
}

// Add field comments if they exist
if len(member.FieldComments) > 0 {
for _, text := range member.FieldComments {
v, err = b.Comment(Comment{
SingleLine: true,
Text: text,
TrailingNewLine: false,
Node: v,
})
if err != nil {
return nil, fmt.Errorf("comment field %q: %w", member.Name, err)
}
}
}

members = append(members, v)
}

res, err := typeLiteralF(goja.Undefined(), b.vm.NewArray(members...))
if err != nil {
return nil, xerrors.Errorf("call typeLiteralNode: %w", err)
}

return res.ToObject(b.vm), nil
}

func (b *Bindings) TypeIntersection(node *TypeIntersection) (*goja.Object, error) {
intersectionF, err := b.f("intersectionType")
if err != nil {
return nil, err
}

var types []interface{}
for _, t := range node.Types {
v, err := b.ToTypescriptExpressionNode(t)
if err != nil {
return nil, fmt.Errorf("intersection type: %w", err)
}
types = append(types, v)
}

res, err := intersectionF(goja.Undefined(), b.vm.NewArray(types...))
if err != nil {
return nil, xerrors.Errorf("call intersectionType: %w", err)
}
return res.ToObject(b.vm), nil
}
15 changes: 15 additions & 0 deletions bindings/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,18 @@ type EnumMember struct {

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

// TypeLiteralNode represents an object type literal like { name: string }
type TypeLiteralNode struct {
Members []*PropertySignature
}

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

type TypeIntersection struct {
Types []ExpressionType
}

func (*TypeIntersection) isNode() {}
func (*TypeIntersection) isExpressionType() {}
6 changes: 5 additions & 1 deletion bindings/walk/walk.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,17 @@ func Walk(v Visitor, node bindings.Node) {
case *bindings.LiteralType:
// noop
case *bindings.Null:
// noop
// noop
case *bindings.HeritageClause:
walkList(v, n.Args)
case *bindings.OperatorNodeType:
Walk(v, n.Type)
case *bindings.EnumMember:
Walk(v, n.Value)
case *bindings.TypeLiteralNode:
walkList(v, n.Members)
case *bindings.TypeIntersection:
walkList(v, n.Types)
default:
panic(fmt.Sprintf("convert.Walk: unexpected node type %T", n))
}
Expand Down
45 changes: 43 additions & 2 deletions config/mutations.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func TrimEnumPrefix(ts *guts.Typescript) {

// EnumAsTypes uses types to handle enums rather than using 'enum'.
// An enum will look like:
// type EnumString = "bar" | "baz" | "foo" | "qux";
// type EnumString = "bar" | "baz" | "foo" | "qux";
func EnumAsTypes(ts *guts.Typescript) {
ts.ForEach(func(key string, node bindings.Node) {
enum, ok := node.(*bindings.Enum)
Expand Down Expand Up @@ -142,7 +142,7 @@ func EnumAsTypes(ts *guts.Typescript) {
// )
// 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.
// 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) {
Expand Down Expand Up @@ -342,6 +342,47 @@ func (v *notNullMaps) Visit(node bindings.Node) walk.Visitor {
return v
}

// InterfaceToType converts all interfaces to type aliases.
// interface User { name: string } --> type User = { name: string }
func InterfaceToType(ts *guts.Typescript) {
ts.ForEach(func(key string, node bindings.Node) {
intf, ok := node.(*bindings.Interface)
if !ok {
return
}

// Create a type literal node to represent the interface structure
var typeLiteral bindings.ExpressionType = &bindings.TypeLiteralNode{
Members: intf.Fields,
}

// If the interface has heritage (extends/implements), create an intersection type.
// The output of an intersection type is equivalent to extending multiple interfaces.
if len(intf.Heritage) > 0 {
var intersection []bindings.ExpressionType
intersection = make([]bindings.ExpressionType, 0, len(intf.Heritage)+1)
for _, heritage := range intf.Heritage {
for _, arg := range heritage.Args {
intersection = append(intersection, arg)
}
}
intersection = append(intersection, typeLiteral)
typeLiteral = &bindings.TypeIntersection{
Types: intersection,
}
}

// Replace the interface with a type alias
ts.ReplaceNode(key, &bindings.Alias{
Name: intf.Name,
Modifiers: intf.Modifiers,
Type: typeLiteral,
Parameters: intf.Parameters,
Source: intf.Source,
})
})
}

func isGoEnum(n bindings.Node) (*bindings.Alias, *bindings.UnionType, bool) {
al, ok := n.(*bindings.Alias)
if !ok {
Expand Down
2 changes: 2 additions & 0 deletions convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ func TestGeneration(t *testing.T) {
mutations = append(mutations, config.NullUnionSlices)
case "TrimEnumPrefix":
mutations = append(mutations, config.TrimEnumPrefix)
case "InterfaceToType":
mutations = append(mutations, config.InterfaceToType)
default:
t.Fatal("unknown mutation, add it to the list:", m)
}
Expand Down
32 changes: 32 additions & 0 deletions testdata/interfacetotype/interfacetotype.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package codersdk

type Score[T int | float32 | float64] struct {
Points T `json:"points"`
Level int `json:"level"`
}

type User[T comparable] struct {
ID T `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
IsActive bool `json:"is_active"`
}

type Player[ID comparable, P int | float32 | float64] struct {
User[ID]
Score[P]

X int `json:"x"`
Y int `json:"y"`
}

type Address struct {
Street string `json:"street"`
City string `json:"city"`
Country string `json:"country"`
}

type GenericContainer[T any] struct {
Value T `json:"value"`
Count int `json:"count"`
}
36 changes: 36 additions & 0 deletions testdata/interfacetotype/interfacetotype.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Code generated by 'guts'. DO NOT EDIT.

// From codersdk/interfacetotype.go
export type Address = {
street: string;
city: string;
country: string;
};

export type Comparable = string | number | boolean;

// From codersdk/interfacetotype.go
export type GenericContainer<T extends any> = {
value: T;
count: number;
};

// From codersdk/interfacetotype.go
export type Player<ID extends Comparable, P extends number> = User<ID> & Score<P> & {
x: number;
y: number;
};

// From codersdk/interfacetotype.go
export type Score<T extends number> = {
points: T;
level: number;
};

// From codersdk/interfacetotype.go
export type User<T extends Comparable> = {
id: T;
name: string;
email: string;
is_active: boolean;
};
1 change: 1 addition & 0 deletions testdata/interfacetotype/mutations
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
InterfaceToType,ExportTypes,ReadOnly
2 changes: 1 addition & 1 deletion typescript-engine/dist/main.js

Large diffs are not rendered by default.

14 changes: 13 additions & 1 deletion typescript-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// https://astexplorer.net/

import * as ts from "typescript";
import {Identifier, Modifier, ModifierSyntaxKind, StringLiteral} from "typescript";
import {Identifier, IntersectionTypeNode, Modifier, ModifierSyntaxKind, StringLiteral, TypeNode} from "typescript";

type modifierKeys = FilterKeys<typeof ts.SyntaxKind, ModifierSyntaxKind>;
export function modifier(name: modifierKeys | ts.Modifier): Modifier {
Expand Down Expand Up @@ -279,6 +279,16 @@ export function typeOperatorNode(
return ts.factory.createTypeOperatorNode(ts.SyntaxKind[operator], node);
}

export function typeLiteralNode(
members: ts.TypeElement[]
): ts.TypeLiteralNode {
return ts.factory.createTypeLiteralNode(members);
}

export function intersectionType(types:TypeNode[]): ts.IntersectionTypeNode {
return ts.factory.createIntersectionTypeNode(types);
}

module.exports = {
modifier: modifier,
identifier: identifier,
Expand All @@ -305,6 +315,8 @@ module.exports = {
variableDeclarationList: variableDeclarationList,
arrayLiteral: arrayLiteral,
typeOperatorNode: typeOperatorNode,
typeLiteralNode: typeLiteralNode,
intersectionType: intersectionType,
enumDeclaration: enumDeclaration,
enumMember: enumMember,
};