The command-line tool:
go get github.com/GoodNotes/typescriptify-golang-structs/tscriptify
The library:
go get github.com/GoodNotes/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
-all-optional
Set all fields optional
-backup string
Directory where backup files are saved
-camel-case
Convert all field names to camelCase
-import value
Typescript import for your custom type, repeat this option for each import needed
-interface
Create interfaces (not classes)
-local-pkg
Replace github.com/GoodNotes/typescriptify-golang-structs with the current directory in go.mod file. Useful for local development.
-package string
Path of the package with models
-readonly
Set all fields readonly
-target string
Target typescript file
-verbose
Verbose logs
If the Person
structs contain a reference to the Address
struct, then you don't have to add Address
explicitly. Any public field 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 your 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;
}
When working with upstream Golang structs which you can not directly modify, the TagAll()
and AddFieldTags()
methods can be used to add tags to all fields or only specific fields.
In the below example, the AWS CSI Driver SecretDescriptor struct is converted to a JSII compatible TypeScript interface (readonly fields) as well as marking all fields as Optional
.
package main
import (
"fmt"
"reflect"
"github.com/aws/secrets-store-csi-driver-provider-aws/provider"
"github.com/GoodNotes/typescriptify-golang-structs/typescriptify"
)
func main() {
sdType := reflect.TypeOf(provider.SecretDescriptor{})
sdTypeOptional := typescriptify.TagAll(sdType, []string{"omitempty"})
t := typescriptify.
New().
WithReadonlyFields(true).
WithInterface(true).
WithBackupDir("").
AddTypeWithName(sdTypeOptional, "SecretDescriptor")
err := t.ConvertToFile("./driver-provider-aws.secrets-store.csi.x-k8s.io.generated.ts")
if err != nil {
panic(err.Error())
}
fmt.Println("OK")
}
Below snippet shows how to set the field ObjectType
of the above SecretDescriptor
struct to a Union type of literal Types using the AddFieldTags
method:
sdType := reflect.TypeOf(provider.SecretDescriptor{})
// set sd ObjectType to "ssmparameter" | "secretsmanager"
fieldTags := make(typescriptify.FieldTags)
fieldTags["ObjectType"] = []*structtag.Tag{
{
Key: "ts_type",
Name: "\"ssmparameter\" | \"secretsmanager\"",
Options: []string{},
},
}
sdTypeTagged := typescriptify.AddFieldTags(sdType, &fieldTags)
t := typescriptify.
New().
AddTypeWithName(sdTypeTagged, "SecretDescriptor")
Conversion of field names to camelCase can be achieved using the WithCamelCase
method:
type PersonalInfo struct {
Hobbies []string `json:",omitempty"`
PetName string `json:",omitempty"`
}
type CloudKitDev struct {
Name string
PersonalInfo PersonalInfo
}
t := typescriptify.New()
t.CreateInterface = true
t.ReadOnlyFields = true
t.CamelCaseFields = true
t.BackupDir = ""
t.AddType(reflect.TypeOf(CloudKitDev{}))
The resulting code will be:
export interface PersonalInfo {
readonly hobbies?: string[];
readonly petName?: string;
}
export interface CloudKitDev {
readonly name: string;
readonly personalInfo: PersonalInfo;
}
Note: In both of these cases use the
AddTypeWithName
method to explicitly provide the name for the generated TypeScript interface. By designreflect.Type
returns an empty string for non-defined types.
This library is licensed under the Apache License, Version 2.0