typescriptify-golang-structs

module
v0.0.0-...-3418263 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 9, 2022 License: Apache-2.0

README

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

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL