Skip to content

Commit 8d83464

Browse files
stavros-kclaudeEmyrk
authored
feat: add InterfaceToType mutation to convert interfaces to type aliases (#52)
* feat: add InterfaceToType mutation to convert interfaces to type aliases This mutation converts TypeScript interfaces to type aliases with object literals: - interface User { name: string } -> type User = { name: string } - Supports generic interfaces with type parameters - Adds TypeLiteralNode expression type and corresponding bindings - Includes test data and TypeScript engine support - Adds TypeIntersection expression and corresponding bindings --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Steven Masley <stevenmasley@gmail.com>
1 parent 5bb6321 commit 8d83464

File tree

10 files changed

+213
-5
lines changed

10 files changed

+213
-5
lines changed

bindings/bindings.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ func (b *Bindings) ToTypescriptExpressionNode(ety ExpressionType) (*goja.Object,
9999
siObj, err = b.ArrayLiteral(ety)
100100
case *OperatorNodeType:
101101
siObj, err = b.OperatorNode(ety)
102+
case *TypeLiteralNode:
103+
siObj, err = b.TypeLiteralNode(ety)
104+
case *TypeIntersection:
105+
siObj, err = b.TypeIntersection(ety)
102106
default:
103107
return nil, xerrors.Errorf("unsupported type for field type: %T", ety)
104108
}
@@ -703,3 +707,64 @@ func (b *Bindings) EnumDeclaration(e *Enum) (*goja.Object, error) {
703707

704708
return obj, nil
705709
}
710+
711+
func (b *Bindings) TypeLiteralNode(node *TypeLiteralNode) (*goja.Object, error) {
712+
typeLiteralF, err := b.f("typeLiteralNode")
713+
if err != nil {
714+
return nil, err
715+
}
716+
717+
var members []interface{}
718+
for _, member := range node.Members {
719+
v, err := b.PropertySignature(member)
720+
if err != nil {
721+
return nil, err
722+
}
723+
724+
// Add field comments if they exist
725+
if len(member.FieldComments) > 0 {
726+
for _, text := range member.FieldComments {
727+
v, err = b.Comment(Comment{
728+
SingleLine: true,
729+
Text: text,
730+
TrailingNewLine: false,
731+
Node: v,
732+
})
733+
if err != nil {
734+
return nil, fmt.Errorf("comment field %q: %w", member.Name, err)
735+
}
736+
}
737+
}
738+
739+
members = append(members, v)
740+
}
741+
742+
res, err := typeLiteralF(goja.Undefined(), b.vm.NewArray(members...))
743+
if err != nil {
744+
return nil, xerrors.Errorf("call typeLiteralNode: %w", err)
745+
}
746+
747+
return res.ToObject(b.vm), nil
748+
}
749+
750+
func (b *Bindings) TypeIntersection(node *TypeIntersection) (*goja.Object, error) {
751+
intersectionF, err := b.f("intersectionType")
752+
if err != nil {
753+
return nil, err
754+
}
755+
756+
var types []interface{}
757+
for _, t := range node.Types {
758+
v, err := b.ToTypescriptExpressionNode(t)
759+
if err != nil {
760+
return nil, fmt.Errorf("intersection type: %w", err)
761+
}
762+
types = append(types, v)
763+
}
764+
765+
res, err := intersectionF(goja.Undefined(), b.vm.NewArray(types...))
766+
if err != nil {
767+
return nil, xerrors.Errorf("call intersectionType: %w", err)
768+
}
769+
return res.ToObject(b.vm), nil
770+
}

bindings/expressions.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,18 @@ type EnumMember struct {
181181

182182
func (*EnumMember) isNode() {}
183183
func (*EnumMember) isExpressionType() {}
184+
185+
// TypeLiteralNode represents an object type literal like { name: string }
186+
type TypeLiteralNode struct {
187+
Members []*PropertySignature
188+
}
189+
190+
func (*TypeLiteralNode) isNode() {}
191+
func (*TypeLiteralNode) isExpressionType() {}
192+
193+
type TypeIntersection struct {
194+
Types []ExpressionType
195+
}
196+
197+
func (*TypeIntersection) isNode() {}
198+
func (*TypeIntersection) isExpressionType() {}

bindings/walk/walk.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,17 @@ func Walk(v Visitor, node bindings.Node) {
5959
case *bindings.LiteralType:
6060
// noop
6161
case *bindings.Null:
62-
// noop
62+
// noop
6363
case *bindings.HeritageClause:
6464
walkList(v, n.Args)
6565
case *bindings.OperatorNodeType:
6666
Walk(v, n.Type)
6767
case *bindings.EnumMember:
6868
Walk(v, n.Value)
69+
case *bindings.TypeLiteralNode:
70+
walkList(v, n.Members)
71+
case *bindings.TypeIntersection:
72+
walkList(v, n.Types)
6973
default:
7074
panic(fmt.Sprintf("convert.Walk: unexpected node type %T", n))
7175
}

config/mutations.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ func TrimEnumPrefix(ts *guts.Typescript) {
107107

108108
// EnumAsTypes uses types to handle enums rather than using 'enum'.
109109
// An enum will look like:
110-
// type EnumString = "bar" | "baz" | "foo" | "qux";
110+
// type EnumString = "bar" | "baz" | "foo" | "qux";
111111
func EnumAsTypes(ts *guts.Typescript) {
112112
ts.ForEach(func(key string, node bindings.Node) {
113113
enum, ok := node.(*bindings.Enum)
@@ -142,7 +142,7 @@ func EnumAsTypes(ts *guts.Typescript) {
142142
// )
143143
// const MyEnums: string = ["foo", "bar"] <-- this is added
144144
// TODO: Enums were changed to use proper enum types. This should be
145-
// updated to support that. EnumLists only works with EnumAsTypes used first.
145+
// updated to support that. EnumLists only works with EnumAsTypes used first.
146146
func EnumLists(ts *guts.Typescript) {
147147
addNodes := make(map[string]bindings.Node)
148148
ts.ForEach(func(key string, node bindings.Node) {
@@ -342,6 +342,47 @@ func (v *notNullMaps) Visit(node bindings.Node) walk.Visitor {
342342
return v
343343
}
344344

345+
// InterfaceToType converts all interfaces to type aliases.
346+
// interface User { name: string } --> type User = { name: string }
347+
func InterfaceToType(ts *guts.Typescript) {
348+
ts.ForEach(func(key string, node bindings.Node) {
349+
intf, ok := node.(*bindings.Interface)
350+
if !ok {
351+
return
352+
}
353+
354+
// Create a type literal node to represent the interface structure
355+
var typeLiteral bindings.ExpressionType = &bindings.TypeLiteralNode{
356+
Members: intf.Fields,
357+
}
358+
359+
// If the interface has heritage (extends/implements), create an intersection type.
360+
// The output of an intersection type is equivalent to extending multiple interfaces.
361+
if len(intf.Heritage) > 0 {
362+
var intersection []bindings.ExpressionType
363+
intersection = make([]bindings.ExpressionType, 0, len(intf.Heritage)+1)
364+
for _, heritage := range intf.Heritage {
365+
for _, arg := range heritage.Args {
366+
intersection = append(intersection, arg)
367+
}
368+
}
369+
intersection = append(intersection, typeLiteral)
370+
typeLiteral = &bindings.TypeIntersection{
371+
Types: intersection,
372+
}
373+
}
374+
375+
// Replace the interface with a type alias
376+
ts.ReplaceNode(key, &bindings.Alias{
377+
Name: intf.Name,
378+
Modifiers: intf.Modifiers,
379+
Type: typeLiteral,
380+
Parameters: intf.Parameters,
381+
Source: intf.Source,
382+
})
383+
})
384+
}
385+
345386
func isGoEnum(n bindings.Node) (*bindings.Alias, *bindings.UnionType, bool) {
346387
al, ok := n.(*bindings.Alias)
347388
if !ok {

convert_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ func TestGeneration(t *testing.T) {
123123
mutations = append(mutations, config.NullUnionSlices)
124124
case "TrimEnumPrefix":
125125
mutations = append(mutations, config.TrimEnumPrefix)
126+
case "InterfaceToType":
127+
mutations = append(mutations, config.InterfaceToType)
126128
default:
127129
t.Fatal("unknown mutation, add it to the list:", m)
128130
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package codersdk
2+
3+
type Score[T int | float32 | float64] struct {
4+
Points T `json:"points"`
5+
Level int `json:"level"`
6+
}
7+
8+
type User[T comparable] struct {
9+
ID T `json:"id"`
10+
Name string `json:"name"`
11+
Email string `json:"email"`
12+
IsActive bool `json:"is_active"`
13+
}
14+
15+
type Player[ID comparable, P int | float32 | float64] struct {
16+
User[ID]
17+
Score[P]
18+
19+
X int `json:"x"`
20+
Y int `json:"y"`
21+
}
22+
23+
type Address struct {
24+
Street string `json:"street"`
25+
City string `json:"city"`
26+
Country string `json:"country"`
27+
}
28+
29+
type GenericContainer[T any] struct {
30+
Value T `json:"value"`
31+
Count int `json:"count"`
32+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Code generated by 'guts'. DO NOT EDIT.
2+
3+
// From codersdk/interfacetotype.go
4+
export type Address = {
5+
street: string;
6+
city: string;
7+
country: string;
8+
};
9+
10+
export type Comparable = string | number | boolean;
11+
12+
// From codersdk/interfacetotype.go
13+
export type GenericContainer<T extends any> = {
14+
value: T;
15+
count: number;
16+
};
17+
18+
// From codersdk/interfacetotype.go
19+
export type Player<ID extends Comparable, P extends number> = User<ID> & Score<P> & {
20+
x: number;
21+
y: number;
22+
};
23+
24+
// From codersdk/interfacetotype.go
25+
export type Score<T extends number> = {
26+
points: T;
27+
level: number;
28+
};
29+
30+
// From codersdk/interfacetotype.go
31+
export type User<T extends Comparable> = {
32+
id: T;
33+
name: string;
34+
email: string;
35+
is_active: boolean;
36+
};

testdata/interfacetotype/mutations

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
InterfaceToType,ExportTypes,ReadOnly

typescript-engine/dist/main.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

typescript-engine/src/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
// https://astexplorer.net/
1212

1313
import * as ts from "typescript";
14-
import {Identifier, Modifier, ModifierSyntaxKind, StringLiteral} from "typescript";
14+
import {Identifier, IntersectionTypeNode, Modifier, ModifierSyntaxKind, StringLiteral, TypeNode} from "typescript";
1515

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

282+
export function typeLiteralNode(
283+
members: ts.TypeElement[]
284+
): ts.TypeLiteralNode {
285+
return ts.factory.createTypeLiteralNode(members);
286+
}
287+
288+
export function intersectionType(types:TypeNode[]): ts.IntersectionTypeNode {
289+
return ts.factory.createIntersectionTypeNode(types);
290+
}
291+
282292
module.exports = {
283293
modifier: modifier,
284294
identifier: identifier,
@@ -305,6 +315,8 @@ module.exports = {
305315
variableDeclarationList: variableDeclarationList,
306316
arrayLiteral: arrayLiteral,
307317
typeOperatorNode: typeOperatorNode,
318+
typeLiteralNode: typeLiteralNode,
319+
intersectionType: intersectionType,
308320
enumDeclaration: enumDeclaration,
309321
enumMember: enumMember,
310322
};

0 commit comments

Comments
 (0)