Skip to content

Instantly share code, notes, and snippets.

@hasenj
Last active November 10, 2024 16:32
Show Gist options
  • Save hasenj/95dbc2321dc584093537b30289bc5a58 to your computer and use it in GitHub Desktop.
Save hasenj/95dbc2321dc584093537b30289bc5a58 to your computer and use it in GitHub Desktop.
TSBridge: a system to auto generate typescript bindings for Go server side functions
This code is shared as-is. Use at your own risk.
It is not meant to be depended upon as a module. Instead, copy it to your own code base and make changes as needed.
CC0/MIT-0/Unlicense
It generates type definitions for structs and enums in Go to Typescript.
It's only meant to be executed locally; during development.
Example usage:
var s2t tsbridge.Bridge
// Add those in a loop or something
// pass instances of `reflect.Type`
// Only pass struct types
s2t.QueueType(someReflectedType)
// call this once after queueing all the types you explicitly care about
// this will process each type one by one, and queue any other types that
// are referenced until there's nothing further to process
s2t.Process()
// Now we're ready to write out the typescript type definitions
// create a file or otherwise make any object that implements the
// io.Writer interface so you can write to it
var f, _ = os.Create("server_types.ts")
tsbridge.WriteStructTSBinding(&s2t, f)
package tsbridge
import (
"fmt"
"go/ast"
"go/build"
"go/parser"
"go/token"
)
func (b *Bridge) ProcessPackage(pkgPath string) {
pkgInfo, err := build.Import(pkgPath, "", build.FindOnly)
if err != nil {
fmt.Println("ImportDir error:", err)
return
}
fset := token.NewFileSet()
pkgs, error := parser.ParseDir(fset, pkgInfo.Dir, nil, 0)
if error != nil {
fmt.Println("ParseDir error:", error)
return
}
var enumTypesMap = make(map[string]*EnumInfo)
for idx := range b.Enums {
e := &b.Enums[idx]
enumTypesMap[e.Name] = e
}
for _, pkg := range pkgs {
for _, file := range pkg.Files {
// We can get the values of const names from the scope objects
// list, but they will be out of order.
//
// We can get the order of declarations from the file declarations
// list, but we won't know the values for the types
//
// So we grab the values first but store them for retrival for when
// we iterate the declaration blocks (to get the declarations in
// the declaration order)
var nameValues = make(map[string]any)
for _, object := range file.Scope.Objects {
if object.Kind == ast.Con {
nameValues[object.Name] = object.Data
}
}
for _, decl := range file.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok {
continue
}
if genDecl.Tok != token.CONST {
continue
}
var enumInfo *EnumInfo
for _, spec := range genDecl.Specs {
valSpec := spec.(*ast.ValueSpec)
if typeIdent, ok := valSpec.Type.(*ast.Ident); ok {
enumInfo = enumTypesMap[typeIdent.String()]
}
if enumInfo != nil {
for _, nameIdent := range valSpec.Names {
name := nameIdent.String()
if name == "_" {
continue
}
value := nameValues[name]
enumInfo.Consts = append(enumInfo.Consts, ConstValue{
Name: name,
Value: value,
})
}
}
}
}
}
}
}
package tsbridge
// Supporting structs in typescript
import (
"encoding/json"
"fmt"
"io"
"reflect"
"strings"
)
type Bridge struct {
Structs []StructInfo
Enums []EnumInfo
// types we want to process (structs/enums)
ProcessedTypes []reflect.Type
QueuedTypes []reflect.Type
// packages we want to process consts for
QueuedPackages []string
ProcessedPackages []string
}
type StructInfo struct {
Name string
Fields []StructField
}
type StructField struct {
Name string
TypeName string
TypeCustom bool
}
type EnumInfo struct {
Name string
TypeName string
Consts []ConstValue
}
type ConstValue struct {
Name string
Value any // int or string
}
func (b *Bridge) QueueObject(x interface{}) {
b.QueueType(reflect.TypeOf(x))
}
func (b *Bridge) QueueType(t reflect.Type) {
// don't care about native types
if t.Name() == t.Kind().String() {
return
}
// make sure we don't already have it!
for _, pt := range b.QueuedTypes {
if t == pt {
return
}
}
for _, pt := range b.ProcessedTypes {
if t == pt {
return
}
}
// fmt.Println("Adding to queue:", t)
b.QueuedTypes = append(b.QueuedTypes, t)
}
func (b *Bridge) QueuePackage(pkgPath string) {
for _, pp := range b.QueuedPackages {
if pp == pkgPath {
return
}
}
for _, pp := range b.ProcessedPackages {
if pp == pkgPath {
return
}
}
b.QueuedPackages = append(b.QueuedPackages, pkgPath)
}
func (b *Bridge) Process() {
for len(b.QueuedTypes) > 0 {
var t = b.QueuedTypes[0]
b.processType(t)
b.QueuedTypes = b.QueuedTypes[1:]
}
for _, pkgPath := range b.QueuedPackages {
b.ProcessPackage(pkgPath)
}
}
func (b *Bridge) processType(t reflect.Type) {
// fmt.Println("Processing: ", t)
var kind = t.Kind()
if kind == reflect.Struct {
var sinfo StructInfo
sinfo.Name = t.Name()
b.AddStructFields(&sinfo, t)
b.Structs = append(b.Structs, sinfo)
b.ProcessedTypes = append(b.ProcessedTypes, t)
} else {
b.QueuePackage(t.PkgPath())
var einfo EnumInfo
einfo.Name = t.Name()
einfo.TypeName = DecideEnumTypeName(t)
b.Enums = append(b.Enums, einfo)
b.ProcessedTypes = append(b.ProcessedTypes, t)
}
}
func (b *Bridge) AddStructFields(sinfo *StructInfo, t reflect.Type) {
var numFields = t.NumField()
for index := 0; index < numFields; index++ {
var field = t.Field(index)
if field.Anonymous {
b.AddStructFields(sinfo, field.Type)
continue
}
var sField StructField // our data
sField.Name = field.Name
sField.TypeName = field.Type.Name()
var jsonTag = field.Tag.Get("json")
if jsonTag != "" {
var parts = strings.Split(jsonTag, ",")
if parts[0] != "" {
if parts[0] == "-" {
continue
}
sField.Name = parts[0]
}
}
var tsTag = field.Tag.Get("ts")
if tsTag != "" {
var parts = strings.Split(tsTag, ",")
if parts[0] != "" {
sField.TypeName = parts[0]
sField.TypeCustom = true
}
}
if !sField.TypeCustom {
sField.TypeName = b.DecideTypescriptTypeName(field.Type)
}
sinfo.Fields = append(sinfo.Fields, sField)
}
}
func DecideEnumTypeName(t reflect.Type) string {
switch t.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return "number"
case reflect.String:
return "string"
default:
return "void"
}
}
func (b *Bridge) DecideTypescriptTypeName(t reflect.Type) string {
var kind = t.Kind()
// fmt.Println("Type:", t, "Kind:", kind)
switch kind {
case reflect.Struct:
b.QueueType(t)
return t.Name()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
if t.String() != kind.String() {
// potentially an enum!
// fmt.Println(" Maybe an enum!")
b.QueueType(t)
return t.Name()
}
return "number"
case reflect.String:
if t.String() != kind.String() {
// potentially an enum!
// fmt.Println(" Maybe an enum!")
b.QueueType(t)
return t.Name()
}
return "string"
case reflect.Bool:
return "boolean"
case reflect.Slice, reflect.Array:
var elementType = b.DecideTypescriptTypeName(t.Elem())
if elementType == "" {
return ""
}
return elementType + "[] | null"
case reflect.Map:
var elementType = b.DecideTypescriptTypeName(t.Elem())
if elementType == "" {
return ""
}
var keyType = b.DecideTypescriptTypeName(t.Key())
if keyType == "" {
return ""
}
return fmt.Sprintf("Record<%s, %s>", keyType, elementType)
case reflect.Ptr:
var elementType = b.DecideTypescriptTypeName(t.Elem())
if elementType == "" {
return ""
}
return elementType + " | null"
case reflect.Interface:
// TODO: should we create a matching TS interface type?!
return "any"
default:
return ""
}
}
func WriteStructTSBinding(b *Bridge, w io.Writer) {
for index := range b.Enums {
var einfo = &b.Enums[index]
fmt.Fprintf(w, "export type %s = %s;\n", einfo.Name, einfo.TypeName)
for _, c := range einfo.Consts {
b, err := json.Marshal(c.Value)
if err != nil {
fmt.Printf("Could not print out the constant value for %s: got error: %v\n", c.Name, err)
}
fmt.Fprintf(w, "export const %s: %s = %s;\n", c.Name, einfo.Name, b)
}
fmt.Fprintln(w)
}
for index := range b.Structs {
var sinfo = &b.Structs[index]
fmt.Fprintf(w, "export interface %s {\n", sinfo.Name)
for findex := range sinfo.Fields {
var field = &sinfo.Fields[findex]
fmt.Fprintf(w, " %s: %s\n", field.Name, field.TypeName)
}
fmt.Fprintf(w, "}\n\n")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment