Skip to content

chaintraced/typescriptify-golang-structs

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

A Golang JSON to TypeScript model converter

Notice

This is a fork of the https://github.com/tkrajina/typescriptify-golang-structs repository. We forked the repository due to it's low activity and our need to support a case in our codebase. We use a certain Generic structure to determine if the JSON was set or not in POST/PATCH API calls, this causes malformed typescript conversions if not handled correctly.

We've added the possibility to set a configuration with the following fields:

  • GenericStructToFieldMapping - A map containing relationships between generic structs and which field should replace it in the output. See example below on how to use it.
  • ErrorOnDuplicateNames - If set to true, an error will be generated if there are duplicate names in the generated class/interfaces

Installation

The command-line tool:

go get github.com/chaintraced/typescriptify-golang-structs/tscriptify

The library:

go get github.com/chaintraced/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;
}

GenericStructToFieldMapping explanation

Given the following snippet of code, written to be able to determine if a field were set or not by the client. Without this modification it's not possible to know if the client purposely set a value to it's zero value or if it was omitted from the JSON.

type GenericField[T any] struct {
	value T
	IsSet bool `json:"-"` // Set means the value has been set
}

type Dummy struct {
  Greeting string `json:"greeting"`
}

type GenericAddress struct {
	// Used in html
	GenDummy GenericField[Dummy]   `json:"genDummy,omitempty"`
	GenField GenericField[string]  `json:"genField,omitempty"`
	GenText1 GenericField[[]Dummy] `json:"genText,omitempty"`
}

We do not want the typescript to look like so:

interface GenericField[Dummy] {

}
interface GenericField[string] {

}
interface GenericField[[]Dummy] {

}
interface GenericAddress {
  genDummy GenericField[Dummy]
  genField GenericField[string]
  genText GenericField[[]Dummy]
}

But rather:

interface Dummy {
  greeting string
}

interface GenericAddress {
  genDummy Dummy
  genField string
  genText []Dummy
}

This can be achieved by setting a configuration with the GenericStructToFieldMapping flag after initializing the converter:

converter := New()
converter.Configuration = Configuration{
  GenericStructToFieldMapping: map[string]string{"typescriptify.GenericField": "value"},
}

When we do this the generic struct (key) will be ignored in favor of it's field (value).

License

This library is licensed under the Apache License, Version 2.0

About

A Golang struct to TypeScript class/interface converter

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Go 88.7%
  • JavaScript 3.8%
  • TypeScript 3.8%
  • HTML 2.1%
  • Other 1.6%