Giter Club home page Giter Club logo

typescriptify-golang-structs's Introduction

A Golang JSON to TypeScript model converter

Installation

The command-line tool:

go install github.com/tkrajina/typescriptify-golang-structs/tscriptify

The library:

go get github.com/tkrajina/typescriptify-golang-structs

Usage

Use the command line tool:

tscriptify -package=package/with/your/models -target=target_ts_file.ts Model1 Model2

If you need to import a custom type in Typescript, you can pass the import string:

tscriptify -package=package/with/your/models -target=target_ts_file.ts -import="import { Decimal } from 'decimal.js'" Model1 Model2

If all your structs are in one file, you can convert them with:

tscriptify -package=package/with/your/models -target=target_ts_file.ts path/to/file/with/structs.go

Or by using it from your code:

converter := typescriptify.New().
    Add(Person{}).
    Add(Dummy{})
err := converter.ConvertToFile("ts/models.ts")
if err != nil {
    panic(err.Error())
}

Command line options:

$ tscriptify --help
Usage of tscriptify:
-backup string
        Directory where backup files are saved
-package string
        Path of the package with models
-target string
        Target typescript file

Models and conversion

If the Person structs contain a reference to the Address struct, then you don't have to add Address explicitly. Only fields with a valid json tag will be converted to TypeScript models.

Example input structs:

type Address struct {
	City    string  `json:"city"`
	Number  float64 `json:"number"`
	Country string  `json:"country,omitempty"`
}

type PersonalInfo struct {
	Hobbies []string `json:"hobby"`
	PetName string   `json:"pet_name"`
}

type Person struct {
	Name         string       `json:"name"`
	PersonalInfo PersonalInfo `json:"personal_info"`
	Nicknames    []string     `json:"nicknames"`
	Addresses    []Address    `json:"addresses"`
	Address      *Address     `json:"address"`
	Metadata     []byte       `json:"metadata" ts_type:"{[key:string]:string}"`
	Friends      []*Person    `json:"friends"`
}

Generated TypeScript:

export class Address {
    city: string;
    number: number;
    country?: string;

    constructor(source: any = {}) {
        if ('string' === typeof source) source = JSON.parse(source);
        this.city = source["city"];
        this.number = source["number"];
        this.country = source["country"];
    }
}
export class PersonalInfo {
    hobby: string[];
    pet_name: string;

    constructor(source: any = {}) {
        if ('string' === typeof source) source = JSON.parse(source);
        this.hobby = source["hobby"];
        this.pet_name = source["pet_name"];
    }
}
export class Person {
    name: string;
    personal_info: PersonalInfo;
    nicknames: string[];
    addresses: Address[];
    address?: Address;
    metadata: {[key:string]:string};
    friends: Person[];

    constructor(source: any = {}) {
        if ('string' === typeof source) source = JSON.parse(source);
        this.name = source["name"];
        this.personal_info = this.convertValues(source["personal_info"], PersonalInfo);
        this.nicknames = source["nicknames"];
        this.addresses = this.convertValues(source["addresses"], Address);
        this.address = this.convertValues(source["address"], Address);
        this.metadata = source["metadata"];
        this.friends = this.convertValues(source["friends"], Person);
    }

	convertValues(a: any, classs: any, asMap: boolean = false): any {
		if (!a) {
			return a;
		}
		if (a.slice) {
			return (a as any[]).map(elem => this.convertValues(elem, classs));
		} else if ("object" === typeof a) {
			if (asMap) {
				for (const key of Object.keys(a)) {
					a[key] = new classs(a[key]);
				}
				return a;
			}
			return new classs(a);
		}
		return a;
	}
}

If you prefer interfaces, the output is:

export interface Address {
    city: string;
    number: number;
    country?: string;
}
export interface PersonalInfo {
    hobby: string[];
    pet_name: string;
}
export interface Person {
    name: string;
    personal_info: PersonalInfo;
    nicknames: string[];
    addresses: Address[];
    address?: Address;
    metadata: {[key:string]:string};
    friends: Person[];
}

In TypeScript you can just cast your json object in any of those models:

var person = <Person> {"name":"Me myself","nicknames":["aaa", "bbb"]};
console.log(person.name);
// The TypeScript compiler will throw an error for this line
console.log(person.something);

Custom Typescript code

Any custom code can be added to Typescript models:

class Address {
        street : string;
        no : number;
        //[Address:]
        country: string;
        getStreetAndNumber() {
            return street + " " + number;
        }
        //[end]
}

The lines between //[Address:] and //[end] will be left intact after ConvertToFile().

If your custom code contain methods, then just casting yout object to the target class (with <Person> {...}) won't work because the casted object won't contain your methods.

In that case use the constructor:

var person = new Person({"name":"Me myself","nicknames":["aaa", "bbb"]});

If you use golang JSON structs as responses from your API, you may want to have a common prefix for all the generated models:

converter := typescriptify.New().
converter.Prefix = "API_"
converter.Add(Person{})

The model name will be API_Person instead of Person.

Field comments

Field documentation comments can be added with the ts_doc tag:

type Person struct {
	Name string `json:"name" ts_doc:"This is a comment"`
}

Generated typescript:

export class Person {
	/** This is a comment */
	name: string;
}

Custom types

If your field has a type not supported by typescriptify which can be JSONized as is, then you can use the ts_type tag to specify the typescript type to use:

type Data struct {
    Counters map[string]int `json:"counters" ts_type:"CustomType"`
}

...will create:

export class Data {
        counters: CustomType;
}

If the JSON field needs some special handling before converting it to a javascript object, use ts_transform. For example:

type Data struct {
    Time time.Time `json:"time" ts_type:"Date" ts_transform:"new Date(__VALUE__)"`
}

Generated typescript:

export class Date {
	time: Date;

    constructor(source: any = {}) {
        if ('string' === typeof source) source = JSON.parse(source);
        this.time = new Date(source["time"]);
    }
}

In this case, you should always use new Data(json) instead of just casting <Data>json.

If you use a custom type that has to be imported, you can do the following:

converter := typescriptify.New()
converter.AddImport("import Decimal from 'decimal.js'")

This will put your import on top of the generated file.

Global custom types

Additionally, you can tell the library to automatically use a given Typescript type and custom transformation for a type:

converter := New()
converter.ManageType(time.Time{}, TypeOptions{TSType: "Date", TSTransform: "new Date(__VALUE__)"})

If you only want to change ts_transform but not ts_type, you can pass an empty string.

Enums

There are two ways to create enums.

Enums with TSName()

In this case you must provide a list of enum values and the enum type must have a TSName() string method

type Weekday int

const (
	Sunday Weekday = iota
	Monday
	Tuesday
	Wednesday
	Thursday
	Friday
	Saturday
)

var AllWeekdays = []Weekday{ Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, }

func (w Weekday) TSName() string {
	switch w {
	case Sunday:
		return "SUNDAY"
	case Monday:
		return "MONDAY"
	case Tuesday:
		return "TUESDAY"
	case Wednesday:
		return "WEDNESDAY"
	case Thursday:
		return "THURSDAY"
	case Friday:
		return "FRIDAY"
	case Saturday:
		return "SATURDAY"
	default:
		return "???"
	}
}

If this is too verbose for you, you can also provide a list of enums and enum names:

var AllWeekdays = []struct {
	Value  Weekday
	TSName string
}{
	{Sunday, "SUNDAY"},
	{Monday, "MONDAY"},
	{Tuesday, "TUESDAY"},
	{Wednesday, "WEDNESDAY"},
	{Thursday, "THURSDAY"},
	{Friday, "FRIDAY"},
	{Saturday, "SATURDAY"},
}

Then, when converting models AddEnum() to specify the enum:

    converter := New().
        AddEnum(AllWeekdays)

The resulting code will be:

export enum Weekday {
	SUNDAY = 0,
	MONDAY = 1,
	TUESDAY = 2,
	WEDNESDAY = 3,
	THURSDAY = 4,
	FRIDAY = 5,
	SATURDAY = 6,
}
export class Holliday {
	name: string;
	weekday: Weekday;
}

License

This library is licensed under the Apache License, Version 2.0

typescriptify-golang-structs's People

Contributors

abelkuruvilla avatar behroozk avatar jackmac92 avatar klpx avatar maranqz avatar oldbane avatar sampaioletti avatar shackra avatar shamer avatar tkrajina avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

typescriptify-golang-structs's Issues

Coerce 'number' values in createFrom method

Add a + in front of the assignment to coerce a string to a number, unless struct tag "json:",string"" present to avoid issues when sending to the golang json parser

static createFrom(source:any){
    ...             
    result.age=+source["age"]
               ^
}

Third-party structs with type `time.Time` convert to Time empty interface/class

Currently, you need to explicitly add tags like ts_type:"Date" and ts_transform:"new Date(__VALUE__)" in order for the conversion to use the correct Typescript type Date for time.Time Go type. But in regard to third-party structs the developer cannot do this tag addition.

Thus, the library should handle this particular use case automatically.

Also, providing an interface that enables developers to handle these edge cases with third-party structs would be useful, like an .Add() method with additional optional arguments to control ts_type and ts_transform.

We should be able to insert imports on the top of the generated file

I have this issue case where I need to import Decimal from decimal.js because I'm sending currency values back and forth between the frontend and the backend, and I wouldn't want to send strings instead, so, if I pass ts_type:"Decimal" I have this problem where the import does not happen anywhere in the file and the TS file is filled with linting errors and does not work at all.

Having a way to add custom imports would become handy to handle this sort of use cases.

64-bit integer as string

Hi,

So, I am using project to generate TypeScript structures for Protocol Buffers generated GO structures. It works.

But, I am using int64 in Prtocol Buffers, which correctly serialize as Strings, because JavaScript Number is not large enough for int64. So, the workaround is to serialize int64 to string.

That means that this library would have to adjust for this specific scenario to generate string instead of Number.

I have made a change to suite me in my fork, but I really would like to push it over time into this project. so, I am opening this issue to discuss the most desired solution.

Mine is pretty straightforward: kulak@5421143 Unfortunately, I have changed the package name to mine to be able to work with the package today. So, I cannot make Pull Request without cleanup.

Thank you

imports .: import cycle not allowed / Can`t handle package "."

go version go1.17 darwin/amd64

tscriptify -package=. -target=/Users/lucasloffel/js/ranked/ranked-ui/src/model/test.ts ./test.go
Parsing: ./test.go
go run /var/folders/15/n8y3ck4x6nxb93ymt3gtqr_m0000gn/T/3451915346/typescriptify_722417000.go
package command-line-arguments
        imports .
        imports .: import cycle not allowed
../../../../pkg/mod/github.com/tkrajina/[email protected]/typescriptify/typescriptify.go:12:2: missing go.sum entry for module providing package github.com/tkrajina/go-reflector/reflector (imported by github.com/tkrajina/typescriptify-golang-structs/typescriptify); to add:
        go get github.com/tkrajina/typescriptify-golang-structs/[email protected]

panic: exit status 1

goroutine 1 [running]:
main.handleErr(...)
        /Users/lucasloffel/go/pkg/mod/github.com/tkrajina/[email protected]/tscriptify/main.go:172
main.main()
        /Users/lucasloffel/go/pkg/mod/github.com/tkrajina/[email protected]/tscriptify/main.go:129 +0x9c6
make: *** [ts-ranked-ui-model] Error 2

test.go

package model

type Test struct {
	Name string `json:"peter"`
}

Embedded structs with JSON tags are completely ignored

In Go I have the following struct

type Token struct {
	*jwt.Payload // https://godoc.org/github.com/gbrlsnchs/jwt#Payload
	User         string              `json:"user"`
	Subcripcion  string              `json:"subcripcion"`
	Rules        map[string][]string `json:"rules" ts_type:"Record<string, Array<string>>"`
}

When you marshal that struct into JSON, the fields on *jwt.Payload get marshalled too if they have any data at all, however, typescriptify ignores it due to lack of the JSON tag, if you add the tag, the fields are included as part of another type, like so:

export interface Payload {
    iss?: string;
    sub?: string;
    aud?: string[];
    exp?: Time;
    nbf?: Time;
    iat?: Time;
    jti?: string;
}
export interface Token {
    payload?: Payload;
    user: string;
    subcripcion: string;
    rules: Record<string, Array<string>>;
}

I believe we should imitate how the fields end after a marshalling and output this result instead:

export interface Token {
    iss?: string;
    sub?: string;
    aud?: string[];
    exp?: Time;
    nbf?: Time;
    iat?: Time;
    jti?: string;
    user: string;
    subcripcion: string;
    rules: Record<string, Array<string>>;
}

Translating Golang functions to TypeScript

Could this compiler be extended to translate Golang functions as well as Golang structs to TypeScript?
For example, this Go function could be easily translated to TypeScript:

func add(x f64, y f64) int {
	return x + y
}

This would be the equivalent function in TypeScript:

export function add(x:number,y:number):number{
    return x+y;
}

It is possible to translate a subset of Go to TypeScript in this way, and there are several other features of Go that could be easily translated:

Possible bug with `.ManageType`

so I get'd the new version of this package and generate a few more types for my project, but for some reasons empty interfaces like Time and Decimal (that one was new) were getting generated despite having ts_type set on some fields of some structs and also using the new feature you recently merged and polished.

This is the generated file:

/* Do not change, this code is generated from Golang structs */

import { Decimal } from 'decimal.js'

export interface UpdateInvoicesRequest {
    claves: string[];
    estado: number;
}
export interface InformacionReferencia {
    tipo_doc: number;
    numero: string;
    fecha_emision: Date;
    codigo: number;
    razon: string;
}
export interface OtroCargo {
    tipo_documento: number;
    detalle: string;
    porcentaje: Decimal;
    monto_cargo: Decimal;
}
export interface Exoneracion {
    tipo_documento: number;
    numero_documento: string;
    nombre_institucion: string;
    fecha_emision: Date;
    porcentaje_exoneracion: Decimal;
    monto_exoneracion: Decimal;
}
export interface Impuesto {
    codigo: number;
    codigo_tarifa: number;
    tarifa: Decimal;
    factor_iva?: Decimal;
    monto: Decimal;
    exoneracion?: Exoneracion;
}
export interface CodigoComercial {
    tipo: number;
    codigo: string;
}
export interface LineaDetalle {
    numero_linea: number;
    partida_arancelaria?: string;
    codigos_comerciales: CodigoComercial[];
    codigo_producto: string;
    cantidad: Decimal;
    unidad_medida: string;
    unidad_medida_comercial?: string;
    detalle: string;
    impuesto: Impuesto[];
    impuesto_neto: Decimal;
    precio_unitario: Decimal;
    monto_descuento?: Decimal;
    naturaleza_descuento?: string;
    monto_total: Decimal;
    sub_total: Decimal;
    base_imponible?: Decimal;
    monto_total_linea: Decimal;
}
export interface Receptor {
    nombre: string;
    nombre_comercial: string;
    identificacion_tipo: number;
    identificacion_numero: string;
    identificacion_extranjero: string;
    provincia: number;
    canton: number;
    distrito: number;
    barrio: number;
    otras_senas: string;
    otras_senas_extranjero: string;
    correo_electronico: string;
    telefono_codigo: number;
    telefono_numero: string;
    fax_codigo: number;
    fax_numero: string;
}
export interface InvoiceCreateRequest {
    emisor: string;
    email: string;
    tipo: number;
    codigo_moneda: string;
    tipo_cambio: Decimal;
    actividad_economica: string;
    condicion_venta: number;
    plazo_credito: number;
    caja: number;
    sucursal: number;
    receptor?: Receptor;
    linea_detalle: LineaDetalle[];
    otros_cargos: OtroCargo[];
    informacion_referencia: InformacionReferencia[];
}
export interface UpdatePasswordRequest {
    clave: string;
    token: string;
}
export interface RefreshTokenRequest {
    token: string;
}
export interface CredentialsRequest {
    cuenta: string;
    clave: string;
}
export interface PasswordResetRequest {
    cuenta: string;
}
export interface ForwardInvoiceTo {
    email: string;
}
export interface Consecutivo {
    id: number;
    tipo: number;
    sucursal: number;
    caja: number;
    contador: number;
}
export interface GetCountersResp {
    consecutivos: Consecutivo[];
    cantidad: number;
}
export interface Emisor {
    nombre: string;
    nombre_comercial: string;
    identificacion_tipo: number;
    identificacion_numero: string;
    provincia: number;
    canton: number;
    distrito: number;
    barrio: number;
    otras_senas: string;
    correo_electronico: string;
    telefono_codigo: number;
    telefono_numero: string;
    fax_codigo: number;
    fax_numero: string;
}
export interface SearchIssuersResp {
    issuers: Emisor[];
}
export interface UpdateCounterReq {
    emisor: string;
    tipo: number;
    sucursal: number;
    caja: number;
    top: number;
}
export interface UpdateIssuerInfoReq {
    nombre?: string;
    nombre_comercial?: string;
    id_tipo?: number;
    id_num?: string;
    correo?: string;
    telf_codigo?: number;
    telf_num?: string;
    fax_codigo?: number;
    fax_num?: string;
}
export interface UpdateIssuerLocationReq {
    otras_senas?: string;
    provincia?: number;
    canton?: number;
    distrito?: number;
    barrio?: number;
}
export interface CreateSubscriptionReq {
    identificacion: string;
    identificacion_tipo: number;
    tipo: number;
    nombre: string;
    correo: string;
    primer_usuario_correo: string;
}
export interface SubscriptionIsDueResponse {
    yes: boolean;
}
export interface Decimal { // HERE!!

}
export interface Subscription {
    tipo: number;
    identificacion_tipo: number;
    identificacion_numero: string;
    nombre: string;
    email: string;
    primer_pago?: Date;
    siguiente_pago?: Date;
    fecha_creado: Date;
    fecha_actualizado: Date;
    costo: Decimal;
    adicional: Decimal;
}
export interface ListSubscriptionsResponse {
    suscripciones: Subscription[];
    cantidad: number;
}
export interface UpdateSubscriptionPrice {
    base?: Decimal;
    adicional?: Decimal;
}
export interface UpdateSubscriptionStatus {
    emisores: string[];
    estado: number;
}
export interface Time { // HERE TOO!

}
export interface Token {
    iss?: string;
    sub?: string;
    aud?: string[];
    exp?: Time;
    nbf?: Time;
    iat?: Time;
    jti?: string;
    user: string;
    subcripcion: string;
    rules: {[key: string]: string[]};
}
export interface Resumen {
    total_servicios_gravados: Decimal;
    total_servicios_exentos: Decimal;
    total_servicios_exonerado: Decimal;
    total_mercancias_gravadas: Decimal;
    total_mercancias_exentas: Decimal;
    total_mercancias_exonerada: Decimal;
    total_gravado: Decimal;
    total_exento: Decimal;
    total_exonerado: Decimal;
    total_venta: Decimal;
    total_descuentos: Decimal;
    total_venta_neta: Decimal;
    total_impuesto: Decimal;
    total_iva_devuelto: Decimal;
    total_otros_cargos: Decimal;
    total_comprobante: Decimal;
}
export interface FormaPago {
    nombre: string;
    codigo: number;
}
export interface Documento {
    id: number;
    clave: string;
    consecutivo: string;
    tipo: number;
    caja: number;
    sucursal: number;
    estado: number;
    emisor: Emisor;
    receptor?: Receptor;
    actividad: string;
    fecha_emision: Date;
    condicion_venta: number;
    plazo_credito: number;
    forma_pago: FormaPago[];
    linea_detalle: LineaDetalle[];
    otros_cargos: OtroCargo[];
    informacion_referencia: InformacionReferencia[];
    codigo_moneda: string;
    tipo_cambio: Decimal;
    normativa: string;
    resumenes?: Resumen;
}
export interface Tokens {
    access: string;
    refresh: string;
}

and this is the Golang file:

package main

import (
	"flag"
	"os"
	"path/filepath"
	"time"

	"github.com/sirupsen/logrus"
	"github.com/tkrajina/typescriptify-golang-structs/typescriptify"
	http_transport "gitlab.com/kuecr/fero/backend/monolith/pkg/http"
	"gitlab.com/kuecr/fero/backend/monolith/pkg/json_web_token"
	"gitlab.com/kuecr/fero/backend/monolith/pkg/service"
)

func main() {
	ubicacionPtr := flag.String("salida", "", "archivo ts donde iran los tipos generados")

	flag.Parse()

	archivo, err := filepath.Abs(*ubicacionPtr)
	if err != nil {
		logrus.Panicf("no se pudo obtener ubicacion absoluta: %v", err)
	}

	converter := typescriptify.New()
	// convierte time.Time en Date para Typescript
	converter = converter.ManageType(time.Time{}, typescriptify.TypeOptions{TSType: "Date"})

	converter.Add(http_transport.UpdateInvoicesRequest{})
	converter.Add(http_transport.InvoiceCreateRequest{})
	converter.Add(http_transport.UpdatePasswordRequest{})
	converter.Add(http_transport.RefreshTokenRequest{})
	converter.Add(http_transport.CredentialsRequest{})
	converter.Add(http_transport.PasswordResetRequest{})
	converter.Add(http_transport.ForwardInvoiceTo{})
	converter.Add(http_transport.GetCountersResp{})
	converter.Add(http_transport.SearchIssuersResp{})
	converter.Add(http_transport.UpdateCounterReq{})
	converter.Add(http_transport.UpdateIssuerInfoReq{})
	converter.Add(http_transport.UpdateIssuerLocationReq{})
	converter.Add(http_transport.CreateSubscriptionReq{})
	converter.Add(http_transport.SubscriptionIsDueResponse{})
	converter.Add(http_transport.ListSubscriptionsResponse{})
	converter.Add(http_transport.UpdateSubscriptionPrice{})
	converter.Add(http_transport.UpdateSubscriptionStatus{})
	converter.Add(json_web_token.Token{})
	converter.Add(service.Documento{})
	converter.Add(service.Tokens{})

	converter.AddImport("import { Decimal } from 'decimal.js'")
	converter.BackupDir = ""
	converter.CreateInterface = true

	err = converter.ConvertToFile(archivo)
	if err != nil {
		logrus.Errorf("no se pudo convertir structs a tipos typescript: %v", err)
		os.Exit(1)
	}
}

I want through each struct there and checked that each field with a decimal.Decimal type has its ts_type:"Decimal" tag set and converter = converter.ManageType(time.Time{}, typescriptify.TypeOptions{TSType: "Date"}) seems correct to me, but, yeah, despite that I get empty interfaces (and Decimal as an empty interface is a new error, I've never see that behavior before)

In my fork I tried to reproduce the error with what I thought was the culprid: https://github.com/shackra/typescriptify-golang-structs/blob/weird-bug/typescriptify/typescriptify_test.go#L444

But surprisingly the test runs fine:

โžœ go test ./... -run TestAnonymousStructWithManagedType
?   	_/home/jorge/code/typescriptify-golang-structs/example	[no test files]
?   	_/home/jorge/code/typescriptify-golang-structs/tscriptify	[no test files]
----------------------------------------------------------------------------------------------------

export interface Target {
    my_time: Date;
    price: Decimal;
}
----------------------------------------------------------------------------------------------------
OK:       export interface Target {
OK:           my_time: Date;
OK:           price: Decimal;
OK:       }
tmp ts:  /tmp/728430900.ts
executing: /tmp/728430900.js
--- FAIL: TestAnonymousStructWithManagedType (1.30s)
    typescriptify_test.go:342:
        	Error Trace:	typescriptify_test.go:342
        	            				typescriptify_test.go:319
        	            				typescriptify_test.go:469
        	Error:      	Expected nil, but got: &exec.ExitError{ProcessState:(*os.ProcessState)(0xc00000e640), Stderr:[]uint8(nil)}
        	Test:       	TestAnonymousStructWithManagedType
        	Messages:   	../../../../../tmp/728430900.ts(3,12): error TS2304: Cannot find name 'Decimal'.
FAIL
FAIL	_/home/jorge/code/typescriptify-golang-structs/typescriptify	1.300s
FAIL

Doesn't work for greater than 1D array of pointers

type test1 struct {
A int json:"a"
}

type test2 struct {
A [][]*test1 json:"a"
}

Won't work. Getting error:

Converting type main.test2

  • slice field test2.A
    panic: cannot find type for ptr (a/)

goroutine 1 [running]:
main.main()
../.../......./main.go:25 +0x114
exit status 2

If you change test2.A to 1d array, then works. So multi dimensional array of pointers doesn't seem to work for me

Enum support

Hello and thanks for this nice library.
Does this library support Enums somehow? Should the following be converted to a Typescript Enum https://www.typescriptlang.org/docs/handbook/enums.html? Please check bellow an example.

`type LeaveType string

const(
AnnualLeave LeaveType = "AnnualLeave"
Sick LeaveType = "Sick"
BankHoliday LeaveType = "BankHoliday"
Other LeaveType = "Other"
)`

CamelCase option

For JSII compatibility it would be handy to have a flag that enforces camelCase for methods and properties.

Support maps with object values

Field map[string]CustomStruct `json:"field"`

Should be converted to:

field: {[key: string]: CustomStruct};

At the moment, the way to convert maps is with custom types:

type Data struct {
    Counters map[string]int `json:"counters" ts_type:"{[key: string]: number}"`
}

to interfaces

How are we supose to convert to interfaces instead of classes? Using the code in the readme

converter := typescriptify.New().
    Add(Person{}).
    Add(Dummy{})
err := converter.ConvertToFile("ts/models.ts")
if err != nil {
    panic(err.Error())
}

YAML tag spport

Hello, I've forked the repo and extended it so that YAML tags are also supported, alongside existing JSON support, of course. I'm not sure whether this is something the project could use, but I can make a PR if you guys want.

Here's the commit.

time.Time no longer automatically converted to Date

The README states that This is how now time.Time is managed in the library by default. which was true as of commit c68aad5:

// manage time.Time automatically
result = result.ManageType(time.Time{}, "Date", "new Date(__VALUE__)")

However as of commit a7a821d this seems to be missing.

Can this functionality be restored or should the README be corrected?

Embedded struct with the same name produces same class

User

type User struct {
	makeless_go_model.User

	UserRoleId *iota.UserRole `gorm:"not null,index" json:"userRoleId"`
	UserRole   *UserRole      `json:"userRole"`
}
/* Do not change, this code is generated from Golang structs */


export class UserRole {
    id?: number;
    name?: string;

    static createFrom(source: any = {}) {
        return new UserRole(source);
    }

    constructor(source: any = {}) {
        if ('string' === typeof source) source = JSON.parse(source);
        this.id = source["id"];
        this.name = source["name"];
    }
}
export class Token {
    id: number;
    createdAt: Time;
    updatedAt: Time;
    deletedAt?: DeletedAt;
    token?: string;
    note?: string;
    userId?: number;
    user?: User;
    teamId?: number;
    team?: Team;

    static createFrom(source: any = {}) {
        return new Token(source);
    }

    constructor(source: any = {}) {
        if ('string' === typeof source) source = JSON.parse(source);
        this.id = source["id"];
        this.createdAt = this.convertValues(source["createdAt"], Time);
        this.updatedAt = this.convertValues(source["updatedAt"], Time);
        this.deletedAt = this.convertValues(source["deletedAt"], DeletedAt);
        this.token = source["token"];
        this.note = source["note"];
        this.userId = source["userId"];
        this.user = this.convertValues(source["user"], User);
        this.teamId = source["teamId"];
        this.team = this.convertValues(source["team"], Team);
    }

	convertValues(a: any, classs: any, asMap: boolean = false): any {
	    if (!a) {
	        return a;
	    }
	    if (a.slice) {
	        return (a as any[]).map(elem => this.convertValues(elem, classs));
	    } else if ("object" === typeof a) {
	        if (asMap) {
	            for (const key of Object.keys(a)) {
	                a[key] = new classs(a[key]);
	            }
	            return a;
	        }
	        return new classs(a);
	    }
	    return a;
	}
}
export class TeamInvitation {
    id: number;
    createdAt: Time;
    updatedAt: Time;
    deletedAt?: DeletedAt;
    teamId?: number;
    team?: Team;
    teamUserId?: number;
    teamUser?: TeamUser;
    email?: string;
    expire?: Time;
    accepted?: boolean;

    static createFrom(source: any = {}) {
        return new TeamInvitation(source);
    }

    constructor(source: any = {}) {
        if ('string' === typeof source) source = JSON.parse(source);
        this.id = source["id"];
        this.createdAt = this.convertValues(source["createdAt"], Time);
        this.updatedAt = this.convertValues(source["updatedAt"], Time);
        this.deletedAt = this.convertValues(source["deletedAt"], DeletedAt);
        this.teamId = source["teamId"];
        this.team = this.convertValues(source["team"], Team);
        this.teamUserId = source["teamUserId"];
        this.teamUser = this.convertValues(source["teamUser"], TeamUser);
        this.email = source["email"];
        this.expire = this.convertValues(source["expire"], Time);
        this.accepted = source["accepted"];
    }

	convertValues(a: any, classs: any, asMap: boolean = false): any {
	    if (!a) {
	        return a;
	    }
	    if (a.slice) {
	        return (a as any[]).map(elem => this.convertValues(elem, classs));
	    } else if ("object" === typeof a) {
	        if (asMap) {
	            for (const key of Object.keys(a)) {
	                a[key] = new classs(a[key]);
	            }
	            return a;
	        }
	        return new classs(a);
	    }
	    return a;
	}
}
export class Team {
    id: number;
    createdAt: Time;
    updatedAt: Time;
    deletedAt?: DeletedAt;
    name?: string;
    userId?: number;
    user?: User;
    teamUsers: TeamUser[];
    teamInvitations: TeamInvitation[];
    tokens: Token[];

    static createFrom(source: any = {}) {
        return new Team(source);
    }

    constructor(source: any = {}) {
        if ('string' === typeof source) source = JSON.parse(source);
        this.id = source["id"];
        this.createdAt = this.convertValues(source["createdAt"], Time);
        this.updatedAt = this.convertValues(source["updatedAt"], Time);
        this.deletedAt = this.convertValues(source["deletedAt"], DeletedAt);
        this.name = source["name"];
        this.userId = source["userId"];
        this.user = this.convertValues(source["user"], User);
        this.teamUsers = this.convertValues(source["teamUsers"], TeamUser);
        this.teamInvitations = this.convertValues(source["teamInvitations"], TeamInvitation);
        this.tokens = this.convertValues(source["tokens"], Token);
    }

	convertValues(a: any, classs: any, asMap: boolean = false): any {
	    if (!a) {
	        return a;
	    }
	    if (a.slice) {
	        return (a as any[]).map(elem => this.convertValues(elem, classs));
	    } else if ("object" === typeof a) {
	        if (asMap) {
	            for (const key of Object.keys(a)) {
	                a[key] = new classs(a[key]);
	            }
	            return a;
	        }
	        return new classs(a);
	    }
	    return a;
	}
}
export class TeamUser {
    id: number;
    createdAt: Time;
    updatedAt: Time;
    deletedAt?: DeletedAt;
    teamId?: number;
    team?: Team;
    userId?: number;
    user?: User;
    role?: string;
    teamInvitations: TeamInvitation[];

    static createFrom(source: any = {}) {
        return new TeamUser(source);
    }

    constructor(source: any = {}) {
        if ('string' === typeof source) source = JSON.parse(source);
        this.id = source["id"];
        this.createdAt = this.convertValues(source["createdAt"], Time);
        this.updatedAt = this.convertValues(source["updatedAt"], Time);
        this.deletedAt = this.convertValues(source["deletedAt"], DeletedAt);
        this.teamId = source["teamId"];
        this.team = this.convertValues(source["team"], Team);
        this.userId = source["userId"];
        this.user = this.convertValues(source["user"], User);
        this.role = source["role"];
        this.teamInvitations = this.convertValues(source["teamInvitations"], TeamInvitation);
    }

	convertValues(a: any, classs: any, asMap: boolean = false): any {
	    if (!a) {
	        return a;
	    }
	    if (a.slice) {
	        return (a as any[]).map(elem => this.convertValues(elem, classs));
	    } else if ("object" === typeof a) {
	        if (asMap) {
	            for (const key of Object.keys(a)) {
	                a[key] = new classs(a[key]);
	            }
	            return a;
	        }
	        return new classs(a);
	    }
	    return a;
	}
}
export class User {
    id: number;
    createdAt: Time;
    updatedAt: Time;
    deletedAt?: DeletedAt;
    name?: string;
    email?: string;
    emailVerification?: EmailVerification;
    teamUsers: TeamUser[];
    tokens: Token[];

    static createFrom(source: any = {}) {
        return new User(source);
    }

    constructor(source: any = {}) {
        if ('string' === typeof source) source = JSON.parse(source);
        this.id = source["id"];
        this.createdAt = this.convertValues(source["createdAt"], Time);
        this.updatedAt = this.convertValues(source["updatedAt"], Time);
        this.deletedAt = this.convertValues(source["deletedAt"], DeletedAt);
        this.name = source["name"];
        this.email = source["email"];
        this.emailVerification = this.convertValues(source["emailVerification"], EmailVerification);
        this.teamUsers = this.convertValues(source["teamUsers"], TeamUser);
        this.tokens = this.convertValues(source["tokens"], Token);
    }

	convertValues(a: any, classs: any, asMap: boolean = false): any {
	    if (!a) {
	        return a;
	    }
	    if (a.slice) {
	        return (a as any[]).map(elem => this.convertValues(elem, classs));
	    } else if ("object" === typeof a) {
	        if (asMap) {
	            for (const key of Object.keys(a)) {
	                a[key] = new classs(a[key]);
	            }
	            return a;
	        }
	        return new classs(a);
	    }
	    return a;
	}
}
export class EmailVerification {
    id: number;
    createdAt: Time;
    updatedAt: Time;
    deletedAt?: DeletedAt;
    verified?: boolean;
    userId?: number;
    user?: User;

    static createFrom(source: any = {}) {
        return new EmailVerification(source);
    }

    constructor(source: any = {}) {
        if ('string' === typeof source) source = JSON.parse(source);
        this.id = source["id"];
        this.createdAt = this.convertValues(source["createdAt"], Time);
        this.updatedAt = this.convertValues(source["updatedAt"], Time);
        this.deletedAt = this.convertValues(source["deletedAt"], DeletedAt);
        this.verified = source["verified"];
        this.userId = source["userId"];
        this.user = this.convertValues(source["user"], User);
    }

	convertValues(a: any, classs: any, asMap: boolean = false): any {
	    if (!a) {
	        return a;
	    }
	    if (a.slice) {
	        return (a as any[]).map(elem => this.convertValues(elem, classs));
	    } else if ("object" === typeof a) {
	        if (asMap) {
	            for (const key of Object.keys(a)) {
	                a[key] = new classs(a[key]);
	            }
	            return a;
	        }
	        return new classs(a);
	    }
	    return a;
	}
}
export class DeletedAt {


    static createFrom(source: any = {}) {
        return new DeletedAt(source);
    }

    constructor(source: any = {}) {
        if ('string' === typeof source) source = JSON.parse(source);

    }
}
export class Time {


    static createFrom(source: any = {}) {
        return new Time(source);
    }

    constructor(source: any = {}) {
        if ('string' === typeof source) source = JSON.parse(source);

    }
}
export class User {
    id: number;
    createdAt: Time;
    updatedAt: Time;
    deletedAt?: DeletedAt;
    name?: string;
    email?: string;
    emailVerification?: EmailVerification;
    teamUsers: TeamUser[];
    tokens: Token[];
    userRoleId?: number;
    userRole?: UserRole;

    static createFrom(source: any = {}) {
        return new User(source);
    }

    constructor(source: any = {}) {
        if ('string' === typeof source) source = JSON.parse(source);
        this.id = source["id"];
        this.createdAt = this.convertValues(source["createdAt"], Time);
        this.updatedAt = this.convertValues(source["updatedAt"], Time);
        this.deletedAt = this.convertValues(source["deletedAt"], DeletedAt);
        this.name = source["name"];
        this.email = source["email"];
        this.emailVerification = this.convertValues(source["emailVerification"], EmailVerification);
        this.teamUsers = this.convertValues(source["teamUsers"], TeamUser);
        this.tokens = this.convertValues(source["tokens"], Token);
        this.userRoleId = source["userRoleId"];
        this.userRole = this.convertValues(source["userRole"], UserRole);
    }

	convertValues(a: any, classs: any, asMap: boolean = false): any {
	    if (!a) {
	        return a;
	    }
	    if (a.slice) {
	        return (a as any[]).map(elem => this.convertValues(elem, classs));
	    } else if ("object" === typeof a) {
	        if (asMap) {
	            for (const key of Object.keys(a)) {
	                a[key] = new classs(a[key]);
	            }
	            return a;
	        }
	        return new classs(a);
	    }
	    return a;
	}
}

go 1.18 generics support

It would be great if this tool could convert Go generics to Typescript generics

type SimpleTile struct {
	AuditID string `json:"audit_id,omitempty"`
	Image   string `json:"image"`
}

type WideTile struct {
	SimpleTile
	Title      string `json:"title"`
	ButtonText string `json:"button_text"`
}

type DefaultTile struct {
	SimpleTile
	Title           string `json:"title"`
	ProviderName    string `json:"provider_name"`
	ProviderFavicon string `json:"provider_favicon"`
}

type Teaser[T DefaultTile | SimpleTile | WideTile] struct {
	Tiles []T `json:"tiles"`
}

Doesn't work with vendoring

When I have vendoring enabled:

/tmp/1378569560/typescriptify_967979130.go:7:2: cannot find package "." in:

without it works fine

How to install?

I'm not really go-person.

I'm not sure how to install this tool

Documentation comments

I would like if the following struct:

// Header Item
type HeaderItem struct {
	//Header name
	Name  string `json:"name"`
	//Header value
	Value string `json:"value"`
}

would convert to the following interface

/**
 * Header Item
 */
interface HeaderItem {
   /**
    * Header name
    */
   name: string;

   /**
    * Header value
    */
   value: string;
}

Is it possible? Other option would be to have a special tag:

type HeaderItem struct {
	Name  string `json:"name" doc:"Header name"`
	Value string `json:"value" doc:"Header value"`
        _ struct{} `doc:"Header Item"`
}

Optional fields

  • AllOptional
  • only specific fields optional (defined by custom tags?)

output for pointers are always treated as `omitempty`

Disclaimer: The following has only been tested using WithInterface(true)- as we're only using interfaces.

Pointers behave as if ,omitempty is applied to the field tag.

This example:

type MyDTO struct {
  Example *float64 `json:"example"`
}

Outputs the following interface:

export interface MyDTO {
  example?: number;
}

This would be correct if the field tag was appended with ,omitempty, like so:

type MyDTO struct {
  Example *float64 `json:"example,omitempty"`
}

But without ,omitempty, the output should be:

export interface MyDTO {
  example: number | null;
}

Are there anyway to achieve this?

Also if I've misunderstood anything (I'm very new to GO), I'd like to apologize in advance ๐Ÿ˜ฌ

Tool can't handle packages like ranked-model

package main

import (
	"fmt"

	"github.com/ranked-de/ranked-model"
	"github.com/tkrajina/typescriptify-golang-structs/typescriptify"
)

func main() {
	t := typescriptify.New()
	t.CreateInterface = false
	t.BackupDir=""

	t.Add(ranked-model.Test{}) <----


	err := t.ConvertToFile("/Users/lucasloffel/js/ranked/ranked-ui/src/model/test.ts")
	if err != nil {
		panic(err.Error())
	}
	fmt.Println("OK")
}

Add support for enums in the CLI tool

Following the docs I am doing

type MyEnum string

const (
	Val1 = MyEnum("val1")
	Val2 = MyEnum("val2")
)

var AllMyEnums = []MyEnum{ Val1, Val2 }

func (l MyEnum) TSName() string { 
	return string(l)
}

and now, when I run

tscriptify -package=package/with/your/models -target=target_ts_file.ts path/to/file/with/structs.go

it only generates for the other structs, and ignores the enum

when i looked at the code that is being generated, and added the line t.AddEnum(m.AllMyEnums) manually, it generated the ts file correctly.

panic when array has a declared length

Hey trying to use this with my project, and it has largely been really helpful, but I noticed it panics when a struct value is an array with defined length.

For example this panics due to Filters

type ExampleStruct {
	Active                 bool       `json:"active"`
	Filters                [3]string  `json:"filters"`
}

The above produces the following panic message

panic: Cannot find type for , fideld: vibe

goroutine 1 [running]:
main.main()
        /path/to/my/repo/gen-ts-types.go:13 +0x1694
exit status 2

But if I remove the size requirement from the struct, it works without issue.

type ExampleStruct {
	Active                 bool     `json:"active"`
	Filters                []string `json:"filters"`
}

Even being able to just ignore this size requirement in the typescript types would be useful for me

Prefix affects map keys

If you use a prefix, the prefix is prepended to map keys, including primitives.

type Example struct {
   Variable map[string]string `json:"variable"`
}

func main() {
    typescriptify.New().WithPrefix("Testing").Add(Example{}).ConvertToFile("./output.ts")
}

Will generate something like this:

export class TestingExample {
    variable: {[key: Testingstring]: string};
}

It also affects all subtypes not specified. This could be desirable behavior but it may cause issues if the types are later or previously defined elsewhere.

Usage as a library

To use this as a library in my module I had to do this

go get github.com/tkrajina/typescriptify-golang-structs/typescriptify

Just doing this

go get github.com/tkrajina/typescriptify-golang-structs

Results in an error

module found... but does not contain package

Nested Struct Won't Convert

Hey! Firstly, thank you so much for this project, it is incredibly helpful! :)

When a struct has an implicitly declared struct within it, Typescriptify doesn't know how to handle it and ends up returning nothing.

The struct in question:

type DashboardMetrics struct {
	BusinessDailyTotal struct {
		Monday    float64 `json:"monday" yaml:"monday"`
		Tuesday   float64 `json:"tuesday" yaml:"tuesday"`
		Wednesday float64 `json:"wednesday" yaml:"wednesday"`
		Thursday  float64 `json:"thursday" yaml:"thursday"`
		Friday    float64 `json:"friday" yaml:"friday"`
	} `json:"business_daily_total" yaml:"business_daily_total"`
	BusinessWeeklyTotal float64 `json:"business_weekly_total" yaml:"business_weekly_total"`
}

The generated Typescript:

export class DashboardMetrics {
    business_daily_total: ;
    business_weekly_total: number;

    constructor(source: any = {}) {
        if ('string' === typeof source) source = JSON.parse(source);
        this.business_daily_total = this.convertValues(source["business_daily_total"], );
        this.business_weekly_total = source["business_weekly_total"];
    }

	convertValues(a: any, classs: any, asMap: boolean = false): any {
	    if (!a) {
	        return a;
	    }
	    if (a instanceof Array) {
	        return (a as any[]).map(elem => this.convertValues(elem, classs));
	    } else if ("object" === typeof a) {
	        if (asMap) {
	            for (const key of Object.keys(a)) {
	                a[key] = new classs(a[key]);
	            }
	            return a;
	        }
	        return new classs(a);
	    }
	    return a;
	}
}

Declaring the struct as its own struct first would of course circumvent this, but it'd be great to see some sort of fix for this :) Apologies if this has already been posted as an issue, I couldn't see anything obvious when I did a quick skim!

Struct Array Alias

Hi,

Given a struct:

type DeviceListing struct {
	Identifier        string    `json:"identifier"`
	AccountIdentifier string    `json:"accountIdentifier"`
	ContactEmail      string    `json:"contactEmail"`
}

I create two other alias structs:

type CreateListingResponse DeviceListing

type GetDeviceListingsResponse []DeviceListing

The CreateListingResponse Typescript interface is created correctly; however, for GetDeviceListingsResponse it just outputs:

export interface GetDeviceListingsResponse {

}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.