blob: c9eb09f6c963d9834ea08a3e4cc9d6bafd70a47e [file] [log] [blame]
// Package go2ts is an extremely simple and powerful Go to TypeScript generator.
// It can handle all JSON serializable Go types and also has the ability to
// define TypeScript union types for your enum-like types.
package go2ts
import (
"fmt"
"go/ast"
"io"
"reflect"
"strings"
"go.skia.org/infra/go/go2ts/typescript"
)
// ignoreNilPolicy determines whether or not nil values in Go types should be reflected in the
// output TypeScript types.
//
// For example, the []string Go type will be rendered as the string[] TypeScript type when ignoring
// nil values, or as (string[] | null) when not. The latter is consistent with how json.Marshal
// handles nil slices.
type ignoreNilPolicy int
const (
ignoreNil ignoreNilPolicy = iota
doNotIgnoreNil
)
// forceToStringPolicy determines if a type should be forced into a string.
//
// An option ",string" can be added to the json tag for string, floating point,
// integer, and boolean types and that will force the value to be serialized as
// a string. For example:
//
// Int64String int64 `json:",string"`
type forceToStringPolicy int
const (
forceToString forceToStringPolicy = iota
doNotForceToString
)
// Go2TS writes TypeScript definitions for Go types.
type Go2TS struct {
// typeDeclarations maps any added reflect.Types to their corresponding TypeScript type
// declarations.
typeDeclarations map[reflect.Type]typescript.TypeDeclaration
// typeDeclarationsInOrder holds type declarations in the order they were added, which determines
// the order they will appear in the output TypeScript code. This field and typeDeclarations
// should be kept in sync.
typeDeclarationsInOrder []typescript.TypeDeclaration
// anonymousCount keeps track of the number of anonymous structs we've had to name.
anonymousCount int
// GenerateNominalTypes tells Go2TS to generate nominal TypeScript types for aliased Go types.
// For background on "nominal typing" in TypeScript, see
// https://www.typescriptlang.org/play#example/nominal-typing and
// https://basarat.gitbook.io/typescript/main-1/nominaltyping.
// The TypeScript compiler itself uses this technique to achieve
// a similar effect as with Go's type aliases:
// https://github.com/Microsoft/TypeScript/blob/7b48a182c05ea4dea81bab73ecbbe9e013a79e99/src/compiler/types.ts#L693
GenerateNominalTypes bool
}
// New returns a new *Go2TS.
func New() *Go2TS {
ret := &Go2TS{
typeDeclarations: map[reflect.Type]typescript.TypeDeclaration{},
typeDeclarationsInOrder: []typescript.TypeDeclaration{},
}
return ret
}
func (g *Go2TS) getOrSaveTypeDeclaration(reflectType reflect.Type, typeDeclaration typescript.TypeDeclaration) typescript.TypeDeclaration {
if existingTypeDeclaration, ok := g.typeDeclarations[reflectType]; ok {
return existingTypeDeclaration
}
g.typeDeclarations[reflectType] = typeDeclaration
g.typeDeclarationsInOrder = append(g.typeDeclarationsInOrder, typeDeclaration)
return typeDeclaration
}
// Add a type that needs a TypeScript definition.
//
// See AddToNamespace() for more details.
func (g *Go2TS) Add(v interface{}) {
g.AddToNamespace(v, "")
}
// AddIgnoreNil adds a type that needs a TypeScript definition.
//
// See AddToNamespaceIgnoreNil() for more details.
func (g *Go2TS) AddIgnoreNil(v interface{}) {
g.AddToNamespaceIgnoreNil(v, "")
}
// AddToNamespace adds a type that needs a TypeScript definition to the given TypeScript namespace.
//
// See AddWithNameToNamespace() for more details.
func (g *Go2TS) AddToNamespace(v interface{}, namespace string) {
g.AddWithNameToNamespace(v, "", namespace)
}
// AddToNamespaceIgnoreNil adds a type that needs a TypeScript definition to the given TypeScript
// namespace.
//
// See AddWithNameToNamespaceIgnoreNil() for more details.
func (g *Go2TS) AddToNamespaceIgnoreNil(v interface{}, namespace string) {
g.AddWithNameToNamespaceIgnoreNil(v, "", namespace)
}
// AddMultiple adds multiple types in a single call.
//
// See AddMultipleToNamespace() for more details.
func (g *Go2TS) AddMultiple(values ...interface{}) {
g.AddMultipleToNamespace("", values...)
}
// AddMultipleToNamespace adds multiple types to the given TypeScript namespace in a single call.
//
// Will stop at the first type that fails.
func (g *Go2TS) AddMultipleToNamespace(namespace string, values ...interface{}) {
for _, v := range values {
g.AddToNamespace(v, namespace)
}
}
// AddWithName adds a type that needs a TypeScript definition.
//
// See AddWithNameToNamespace() for more details.
func (g *Go2TS) AddWithName(v interface{}, interfaceName string) {
g.AddWithNameToNamespace(v, interfaceName, "")
}
// AddWithNameIgnoreNil adds a type that needs a TypeScript definition.
//
// See AddWithNameToNamespaceIgnoreNil() for more details.
func (g *Go2TS) AddWithNameIgnoreNil(v interface{}, interfaceName string) {
g.AddWithNameToNamespaceIgnoreNil(v, interfaceName, "")
}
// AddWithNameToNamespace adds a type that needs a TypeScript definition.
//
// The value passed in can be an instance of a type, a reflect.Type, or a
// reflect.Value.
//
// The 'name' supplied will be the TypeScript interface name. If 'interfaceName'
// is the empty string then the Go type name will be used. If the type is of a
// struct that is anonymous it will be given a name of the form "AnonymousN".
//
// If the type is a struct, the fields of the struct will be named following the
// convention for json serialization, including using the json tag if supplied.
// Fields tagged with `json:",omitempty"` will be marked as optional.
//
// There is special handling of time.Time types to be TypeScript "string"s since
// time.Time implements MarshalJSON, see
// https://pkg.go.dev/time?tab=doc#Time.MarshalJSON.
//
// If namespace is non-empty, the type will be added inside a TypeScript
// namespace of that name.
func (g *Go2TS) AddWithNameToNamespace(v interface{}, interfaceName, namespace string) {
g.add(v, interfaceName, namespace, doNotIgnoreNil)
}
// AddWithNameToNamespaceIgnoreNil adds a type that needs a TypeScript definition.
//
// This method is identical to AddWithNameToNamespace, with the exception that any nillable values
// will be ignored (e.g. string slices will be treated as arrays, maps won't be nullable, etc.).
func (g *Go2TS) AddWithNameToNamespaceIgnoreNil(v interface{}, interfaceName, namespace string) {
g.add(v, interfaceName, namespace, ignoreNil)
}
func (g *Go2TS) add(v interface{}, interfaceName, namespace string, ignoreNilPolicy ignoreNilPolicy) {
var reflectType reflect.Type
switch v := v.(type) {
case reflect.Type:
reflectType = v
case reflect.Value:
reflectType = v.Type()
default:
reflectType = reflect.TypeOf(v)
}
g.addTypeDeclaration(reflectType, interfaceName, namespace, ignoreNilPolicy)
}
// AddUnion adds a TypeScript definition for a union type of the values in 'v',
// which must be a slice or an array.
//
// See AddUnionToNamespace() for more details.
func (g *Go2TS) AddUnion(v interface{}) {
g.AddUnionToNamespace(v, "")
}
// AddUnionToNamespace adds a TypeScript definition for a union type of the values in 'v',
// which must be a slice or an array, to the given TypeScript namespace.
//
// See AddUnionWithNameToNamespace() for more details.
func (g *Go2TS) AddUnionToNamespace(v interface{}, namespace string) {
g.AddUnionWithNameToNamespace(v, "", namespace)
}
// AddMultipleUnion adds multple union types.
//
// See AddMultipleUnionToNamespace() for more details.
func (g *Go2TS) AddMultipleUnion(values ...interface{}) {
g.AddMultipleUnionToNamespace("", values...)
}
// AddMultipleUnionToNamespace adds multple union types to the given TypeScript namespace.
//
// Will stop at the first union type that fails.
func (g *Go2TS) AddMultipleUnionToNamespace(namespace string, values ...interface{}) {
for _, v := range values {
g.AddUnionToNamespace(v, namespace)
}
}
// AddUnionWithName adds a TypeScript definition for a union type of the values
// in 'v', which must be a slice or an array.
//
// See AddUnionWithNameToNamespace() for more details.
func (g *Go2TS) AddUnionWithName(v interface{}, typeName string) {
g.AddUnionWithNameToNamespace(v, typeName, "")
}
// AddUnionWithNameToNamespace adds a TypeScript definition for a union type of
// the values in 'v', which must be a slice or an array, to the given namespace.
//
// If typeName is the empty string then the name of type of elements in the
// slice or array is used as the type name, otherwise the typeName supplied will
// be used as the TypeScript type name.
func (g *Go2TS) AddUnionWithNameToNamespace(v interface{}, typeName, namespace string) {
// We can only build union types from Go slices or arrays.
reflectType := reflect.TypeOf(v)
if reflectType.Kind() != reflect.Slice && reflectType.Kind() != reflect.Array {
panic(fmt.Sprintf("AddUnionWithName must be supplied an array or slice, got %v: %v", reflectType.Kind(), v))
}
// Make sure we have a name for the union type.
if typeName == "" {
typeName = reflectType.Elem().Name()
}
// We will populate the union type with the typescript.LiteralTypes corresponding to the elements
// in the passed in Go slice.
unionType := &typescript.UnionType{
Types: []typescript.Type{},
}
// Iterate over all elements in the passed in Go slice.
values := reflect.ValueOf(v)
for i := 0; i < values.Len(); i++ {
value := values.Index(i)
// Obtain the typescript.BasicType corresponding to the current element. We only support basic
// types; any other types will result in a panic.
var basicType typescript.BasicType
if value.Kind() == reflect.Bool {
basicType = typescript.Boolean
} else if isNumber(value.Kind()) {
basicType = typescript.Number
} else if value.Kind() == reflect.String {
basicType = typescript.String
} else {
panic(fmt.Sprintf("Go Kind %q cannot be used in a TypeScript union type.", value.Kind()))
}
// Create a typescript.LiteralType for the current element and add it to the union type.
unionType.Types = append(unionType.Types, &typescript.LiteralType{
BasicType: basicType,
Literal: fmt.Sprintf("%v", values.Index(i).Interface()),
})
}
if existingTypeDeclaration, ok := g.typeDeclarations[reflectType.Elem()]; ok {
// The reflect.Type was already added, so if it's a TypeScript type alias, we'll update it to be
// an alias for the newly added union type.
existingTypeAliasDeclaration, ok := existingTypeDeclaration.(*typescript.TypeAliasDeclaration)
if !ok {
panic(fmt.Sprintf("Go type %v was already added as something other than a TypeScript type alias.", reflectType.Elem()))
}
existingTypeAliasDeclaration.Namespace = namespace
existingTypeAliasDeclaration.Identifier = typeName
existingTypeAliasDeclaration.Type = unionType
} else {
// The reflect.Type hasn't been seen before, so we declare a new type alias for the union type.
g.getOrSaveTypeDeclaration(reflectType.Elem(), &typescript.TypeAliasDeclaration{
Namespace: namespace,
Identifier: typeName,
Type: unionType,
GenerateNominalTypes: g.GenerateNominalTypes,
})
}
}
// Render the TypeScript definitions to the given io.Writer.
func (g *Go2TS) Render(w io.Writer) error {
if _, err := fmt.Fprintln(w, "// DO NOT EDIT. This file is automatically generated."); err != nil {
return err
}
// Output TypeScript interfaces first.
for _, typeDeclaration := range g.typeDeclarationsInOrder {
if _, ok := typeDeclaration.(*typescript.InterfaceDeclaration); !ok {
continue
}
if _, err := fmt.Fprintln(w); err != nil {
return err
}
if _, err := fmt.Fprintln(w, typeDeclaration.ToTypeScript()); err != nil {
return err
}
}
// Output any other type definitions (e.g. type aliases) second.
for _, typeDeclaration := range g.typeDeclarationsInOrder {
if _, ok := typeDeclaration.(*typescript.InterfaceDeclaration); ok {
continue
}
if _, err := fmt.Fprintln(w); err != nil {
return err
}
if _, err := fmt.Fprintln(w, typeDeclaration.ToTypeScript()); err != nil {
return err
}
}
return nil
}
func (g *Go2TS) addTypeDeclaration(reflectType reflect.Type, typeName, namespace string, ignoreNilPolicy ignoreNilPolicy) {
// Struct types are declared as TypeScript interfaces.
if removeIndirection(reflectType).Kind() == reflect.Struct {
g.addInterfaceDeclaration(reflectType, typeName, namespace, ignoreNilPolicy)
return
}
// All other type declarations are handled as type aliases (except for union types, which are
// handled separately).
if _, ok := g.typeDeclarations[reflectType]; ok {
return
}
if typeName == "" {
typeName = reflectType.Name()
}
typeDeclaration := &typescript.TypeAliasDeclaration{
Namespace: namespace,
Identifier: typeName,
Type: g.reflectTypeToTypeScriptType(reflectType, namespace, ignoreNilPolicy, explicitlyDiscovered, doNotForceToString),
GenerateNominalTypes: g.GenerateNominalTypes,
}
g.getOrSaveTypeDeclaration(reflectType, typeDeclaration)
}
func (g *Go2TS) addInterfaceDeclaration(structType reflect.Type, interfaceName, namespace string, ignoreNilPolicy ignoreNilPolicy) *typescript.InterfaceDeclaration {
structType = removeIndirection(structType)
// Only structs can be declared as TypeScript interfaces.
if structType.Kind() != reflect.Struct {
panic(fmt.Sprintf(`Go Kind %q cannot be declared as a TypeScript interface.`, structType.Kind()))
}
// Nothing to do if the TypeScript interface has already been declared.
if existingTypeDeclaration, ok := g.typeDeclarations[structType]; ok {
return existingTypeDeclaration.(*typescript.InterfaceDeclaration)
}
// Make sure we have a name for the interface, which could be anonymous.
if interfaceName == "" {
interfaceName = strings.Title(structType.Name())
}
if interfaceName == "" {
interfaceName = g.getAnonymousInterfaceName()
}
// Create the interface declaration.
interfaceDeclaration := &typescript.InterfaceDeclaration{
Namespace: namespace,
Identifier: interfaceName,
Properties: []typescript.PropertySignature{},
}
// Save the interface declaration before populating its fields. This guarantees that we won't get
// stuck in an infinite recursion if the Go struct is recursive (e.g. type Foo struct { F *Foo }).
g.typeDeclarations[structType] = interfaceDeclaration
// Populate the interface fields. This will recurse into any embedded structs.
g.populateInterfaceDeclarationProperties(interfaceDeclaration, structType, ignoreNilPolicy, doNotRecursivelyForceOptional)
// Add the interface declaration to the ordered output after populating its fields. This ensures
// that any new types discovered while populating the interface fields will appear before the
// interface declaration in the output TypeScript code.
g.typeDeclarationsInOrder = append(g.typeDeclarationsInOrder, interfaceDeclaration)
return interfaceDeclaration
}
func (g *Go2TS) getAnonymousInterfaceName() string {
g.anonymousCount++
return fmt.Sprintf("Anonymous%d", g.anonymousCount)
}
// optionalFieldPolicy determines whether or not the properties of a TypeScript interface generated
// from a Go struct should be recursively marked as optional.
//
// The recursion occurs in the case of a struct that embeds another struct, in which case the two
// structs are flattened into a single TypeScript interface.
type optionalFieldPolicy int
const (
recursivelyForceOptional optionalFieldPolicy = iota
doNotRecursivelyForceOptional
)
// populateInterfaceDeclarationProperties recursively populates the properties of the given
// interface declaration. It assumes reflectType's Kind is Struct.
//
// If the optionalFieldPolicy is recursivelyForceOptional, any properties populated on
// this or any recursive calls to this method will be marked as optional.
func (g *Go2TS) populateInterfaceDeclarationProperties(interfaceDeclaration *typescript.InterfaceDeclaration, structType reflect.Type, ignoreNilPolicy ignoreNilPolicy, optionalFieldPolicy optionalFieldPolicy) {
isEmbeddedStruct := func(f reflect.StructField) bool {
return f.Anonymous && removeIndirection(f.Type).Kind() == reflect.Struct
}
// Iterate over normal fields first, and embedded structs last. This ensures that outer fields
// will take precedence over inner fields in the case of overlapping fields, which is consistent
// with json.Marshal().
var normalFields, embeddedStructFields []reflect.StructField
for i := 0; i < structType.NumField(); i++ {
field := structType.Field(i)
if isEmbeddedStruct(field) {
embeddedStructFields = append(embeddedStructFields, field)
} else {
normalFields = append(normalFields, field)
}
}
allFields := append(normalFields, embeddedStructFields...)
for _, structField := range allFields {
// Skip unexported fields.
if len(structField.Name) == 0 || !ast.IsExported(structField.Name) {
continue
}
// If the field is an embedded struct, or an embedded struct pointer, we add the inner struct's
// fields to the outer struct (i.e. we flatten the structs). This is consistent with
// json.Marshal().
if isEmbeddedStruct(structField) {
// If the field is an embedded struct pointer, we recursively mark all its fields as optional.
// This is because json.Marshal() will omit said fields if the embedded struct pointer is nil.
embeddedStructOptionalFieldPolicy := doNotRecursivelyForceOptional
if optionalFieldPolicy == recursivelyForceOptional || structField.Type.Kind() == reflect.Ptr {
embeddedStructOptionalFieldPolicy = recursivelyForceOptional
}
g.populateInterfaceDeclarationProperties(interfaceDeclaration, removeIndirection(structField.Type), ignoreNilPolicy, embeddedStructOptionalFieldPolicy)
continue
}
// Read the field's `json:...` tag.
jsonTag := strings.Split(structField.Tag.Get("json"), ",")
// Read the property name from the `json:...` tag, or default to the field name.
propertyName := structField.Name
if len(jsonTag) > 0 && jsonTag[0] != "" {
propertyName = jsonTag[0]
}
// Process all the remaining options that we care about.
markedAsOptional := false
forceString := doNotForceToString
for _, option := range jsonTag[1:] {
if option == "string" {
forceString = forceToString
}
if option == "omitempty" {
markedAsOptional = true
}
}
// A `json:"-"` tag means the field will not be serialized to JSON, so we can skip it.
if propertyName == "-" {
continue
}
// If there's already a field with the same name as the current field, we skip it. This can
// happen when populating the fields of an embedded struct, and the inner and outer structs
// have overlapping fields, in which case the outer fields takes precedence.
overlaps := false
for _, property := range interfaceDeclaration.Properties {
if propertyName == property.Identifier {
overlaps = true
break
}
}
if overlaps {
continue
}
// A `go2ts:"ignorenil"` tag means that any nillable types will be treated as their non-nillable
// counterparts when recursively computing the TypeScript type of the current field. Concretely,
// this means that pointers will have the indirection removed, slices will be treated as
// arrays, maps won't be nullable, etc.
//
// Note that "ignorenil" propagates recursively, meaning that any previously unseen types will
// be added with their nil types ignored. For example, if the current field has type Foo,
// defined as "type Foo []string", and it's annotated with `go2ts:"ignorenil"`, then the
// TypeScript type Foo will be declared as "type Foo = string[]" instead of
// "type Foo = string[] | null".
hasIgnoreNilTag := structField.Tag.Get("go2ts") == "ignorenil"
// A `go2ts:"string"` tag means that the field is serialized as a
// string. Use this if a custom JSON Marshaler has been defined for the
// type. Note that this is different from just adding ",string" for the
// json options of a int, float, or bool type, which is also supported
// by the go2ts program but which presumes a custom Marshaler has not
// been provided.
if structField.Tag.Get("go2ts") == "string" {
forceString = forceToString
}
// Recursively compute the property's TypeScript type.
propertyIgnoreNilPolicy := doNotIgnoreNil
if ignoreNilPolicy == ignoreNil || hasIgnoreNilTag {
propertyIgnoreNilPolicy = ignoreNil
}
propertyType := g.reflectTypeToTypeScriptType(structField.Type, interfaceDeclaration.Namespace, propertyIgnoreNilPolicy, implicitlyDiscovered, forceString)
// Create the property signature and add it to the interface declaration.
property := typescript.PropertySignature{
Identifier: propertyName,
Type: propertyType,
Optional: optionalFieldPolicy == recursivelyForceOptional || markedAsOptional,
}
interfaceDeclaration.Properties = append(interfaceDeclaration.Properties, property)
}
}
// typeDiscovery indicates whether a Go type was explicitly added to a Go2TS instance via one of
// the Add* methods, or implicitly, e.g. by discovering a user-defined type when inspecting the
// field types of a Go struct type explicitly added by the user.
type typeDiscovery int
const (
explicitlyDiscovered typeDiscovery = iota
implicitlyDiscovered
)
func (g *Go2TS) reflectTypeToTypeScriptType(reflectType reflect.Type, namespace string, ignoreNilPolicy ignoreNilPolicy, typeDiscovery typeDiscovery, forceString forceToStringPolicy) typescript.Type {
// If the type is a pointer, then we remove the pointer indirection, compute the resulting
// TypeScript type, and return the union between that type and null.
if reflectType.Kind() == reflect.Ptr {
tsType := g.reflectTypeToTypeScriptType(removeIndirection(reflectType), namespace, ignoreNilPolicy, typeDiscovery, forceString)
if ignoreNilPolicy == ignoreNil {
return tsType
}
return &typescript.UnionType{
Types: []typescript.Type{tsType, typescript.Null},
}
}
// If we have declared this type before, then we just return a reference to the declared type.
if existingTypeDeclaration, ok := g.typeDeclarations[reflectType]; ok {
return existingTypeDeclaration.TypeReference()
}
// Structs are declared as interfaces (save for time.Time, which is a special case handled below).
if reflectType.Kind() == reflect.Struct && !isTime(reflectType) {
return g.addInterfaceDeclaration(reflectType, "", namespace, ignoreNilPolicy).TypeReference()
}
// Will hold the typescript.Type extracted from the reflect.Type.
var tsType typescript.Type
// Compute the TypeScript type based on the Kind of the reflected type.
switch reflectType.Kind() {
case reflect.Uint8,
reflect.Uint16,
reflect.Uint32,
reflect.Uint64,
reflect.Uint,
reflect.Int8,
reflect.Int16,
reflect.Int32,
reflect.Int64,
reflect.Int,
reflect.Float32,
reflect.Float64:
if forceString == forceToString {
tsType = typescript.String
} else {
tsType = typescript.Number
}
case reflect.String:
tsType = typescript.String
case reflect.Bool:
if forceString == forceToString {
tsType = typescript.String
} else {
tsType = typescript.Boolean
}
case reflect.Map:
// TypeScript index signature parameter types[1] must be either "string" or "number", and
// cannot be type aliases, otherwise the TypeScript compiler will fail with error
// "TS1336: An index signature parameter type cannot be a type alias.".
//
// Example:
//
// export type Foo = string;
// export type Bar = { [key: Foo]: string }; // Compiler produces error TS1336.
//
// Thus, we treat map keys as a special case where we ignore any type aliases and use either
// "string" or "number" directly.
//
// [1] https://www.typescriptlang.org/docs/handbook/advanced-types.html#index-types-and-index-signatures.
var indexType typescript.Type
if reflectType.Key().Kind() == reflect.String {
indexType = typescript.String
} else if isNumber(reflectType.Key().Kind()) {
indexType = typescript.Number
} else {
panic(fmt.Sprintf("Go Kind %q cannot be used as a TypeScript index signature parameter type.", reflectType.Key().Kind()))
}
tsType = &typescript.MapType{
IndexType: indexType,
ValueType: g.reflectTypeToTypeScriptType(reflectType.Elem(), namespace, ignoreNilPolicy, implicitlyDiscovered, doNotForceToString),
}
// Maps can be nil.
if ignoreNilPolicy == doNotIgnoreNil {
tsType = &typescript.UnionType{
Types: []typescript.Type{tsType, typescript.Null},
}
}
case reflect.Slice, reflect.Array:
tsType = &typescript.ArrayType{
ItemsType: g.reflectTypeToTypeScriptType(reflectType.Elem(), namespace, ignoreNilPolicy, implicitlyDiscovered, doNotForceToString),
}
// Slices can be nil, but not arrays.
if reflectType.Kind() == reflect.Slice && ignoreNilPolicy == doNotIgnoreNil {
tsType = &typescript.UnionType{
Types: []typescript.Type{tsType, typescript.Null},
}
}
case reflect.Struct:
// This is necessarily a time.Time because we handled all other structs earlier.
tsType = typescript.String
case reflect.Interface:
tsType = typescript.Any
case reflect.Complex64,
reflect.Complex128,
reflect.Chan,
reflect.Func,
reflect.UnsafePointer:
panic(fmt.Sprintf("Go Kind %q cannot be serialized to JSON.", reflectType.Kind()))
}
// If this is a named Go type (e.g. "Donut", assuming we have added a "type Donut string" Go type)
// then we want to declare it as a TypeScript type alias (e.g. "export type Donut = string"), and
// this function should return a reference to the alias (e.g "Donut") instead of the underlying
// type (e.g. "string").
//
// Note that we only want to do this if the type in question wasn't added explicitly via a call to
// one of the Go2TS.Add*() methods because said methods will add the type declarations themselves.
if typeDiscovery == implicitlyDiscovered &&
// All type aliases have a non-empty name.
reflectType.Name() != "" &&
// But not all types with non-empty names are aliases (e.g. the name for the int type is "int").
(!isPrimitive(reflectType.Kind()) || isPrimitiveAlias(reflectType)) &&
// We don't want an alias for time.Time because we treat it as a string in TypeScript.
!isTime(reflectType) {
typeDeclaration := &typescript.TypeAliasDeclaration{
Namespace: namespace,
Identifier: reflectType.Name(),
Type: tsType,
GenerateNominalTypes: g.GenerateNominalTypes,
}
// If we've already added a TypeScript type declaration for this Go type, we'll return a
// reference to the existing declaration, otherwise we'll return a reference to the new
// declaration.
return g.getOrSaveTypeDeclaration(reflectType, typeDeclaration).TypeReference()
}
return tsType
}
func removeIndirection(reflectType reflect.Type) reflect.Type {
kind := reflectType.Kind()
// Follow all the pointers until we get to a non-Ptr kind.
for kind == reflect.Ptr {
reflectType = reflectType.Elem()
kind = reflectType.Kind()
}
return reflectType
}
func isTime(t reflect.Type) bool {
return t.Name() == "Time" && t.PkgPath() == "time"
}
// numbers is the set of Kinds that we convert into the TypeScript "number" type.
var numberKinds = map[reflect.Kind]bool{
reflect.Int: true,
reflect.Int8: true,
reflect.Int16: true,
reflect.Int32: true,
reflect.Int64: true,
reflect.Uint: true,
reflect.Uint8: true,
reflect.Uint16: true,
reflect.Uint32: true,
reflect.Uint64: true,
reflect.Float32: true,
reflect.Float64: true,
}
func isNumber(kind reflect.Kind) bool {
return numberKinds[kind]
}
// nonNumberPrimitiveKinds is the non-number set of Kinds that we support converting to TypeScript.
var nonNumberPrimitiveKinds = map[reflect.Kind]bool{
reflect.Bool: true,
reflect.Uintptr: true,
reflect.String: true,
}
func isPrimitive(kind reflect.Kind) bool {
return numberKinds[kind] || nonNumberPrimitiveKinds[kind]
}
func isPrimitiveAlias(reflectType reflect.Type) bool {
return isPrimitive(reflectType.Kind()) && reflectType.Name() != reflectType.Kind().String()
}