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
The command-line tool:
go get github.com/chaintraced/typescriptify-golang-structs/tscriptify
The library:
go get github.com/chaintraced/typescriptify-golang-structs
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
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);
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 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;
}
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.
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.
There are two ways to create enums.
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;
}
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).
This library is licensed under the Apache License, Version 2.0