Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions v2/internal/binding/binding_test/binding_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func TestBindings_GenerateModels(t *testing.T) {
GeneratedJsEntityWithStringEnumTest,
GeneratedJsEntityWithEnumTsName,
GeneratedJsEntityWithNestedStructInterfacesTest,
NullableCollectionsInterfacesTest,
AnonymousSubStructTest,
AnonymousSubStructMultiLevelTest,
GeneratedJsEntityWithNestedStructTest,
Expand Down
38 changes: 38 additions & 0 deletions v2/internal/binding/binding_test/binding_tsgeneration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,3 +507,41 @@ var GeneratedJsEntityWithNestedStructInterfacesTest = BindingTest{
}
`,
}

type NullableCollectionsEntity struct {
Names []string `json:"names"`
Values map[string]int `json:"values"`
Children []ChildEntity `json:"children"`
Fixed [2]string `json:"fixed"`
}

func (e NullableCollectionsEntity) Get() NullableCollectionsEntity {
return e
}

var NullableCollectionsInterfacesTest = BindingTest{
name: "NullableCollectionsInterfacesTest",
structs: []interface{}{
&NullableCollectionsEntity{},
},
exemptions: nil,
shouldError: false,
TsGenerationOptionsTest: TsGenerationOptionsTest{
TsOutputType: "interfaces",
},
want: `export namespace binding_test {

export interface ChildEntity {
name: string;
childProp: number;
}
export interface NullableCollectionsEntity {
names: string[] | null;
values: Record<string, number> | null;
children: ChildEntity[] | null;
fixed: string[];
}

}
`,
}
37 changes: 27 additions & 10 deletions v2/internal/typescriptify/typescriptify.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ func (t *TypeScriptify) AddType(typeOf reflect.Type) *TypeScriptify {
return t
}

func (t *typeScriptClassBuilder) AddMapField(fieldName string, field reflect.StructField) {
func (t *typeScriptClassBuilder) AddMapField(fieldName string, field reflect.StructField, nullable bool) {
keyType := field.Type.Key()
valueType := field.Type.Elem()
valueTypeName := nameTypeOf(valueType)
Expand Down Expand Up @@ -327,7 +327,11 @@ func (t *typeScriptClassBuilder) AddMapField(fieldName string, field reflect.Str
fieldName = fmt.Sprintf(`"%s"?`, strippedFieldName)
}
}
t.fields = append(t.fields, fmt.Sprintf("%s%s: Record<%s, %s>;", t.indent, fieldName, keyTypeStr, valueTypePrefix+valueTypeName+valueTypeSuffix))
fieldType := fmt.Sprintf("Record<%s, %s>", keyTypeStr, valueTypePrefix+valueTypeName+valueTypeSuffix)
if nullable {
fieldType += " | null"
}
t.fields = append(t.fields, fmt.Sprintf("%s%s: %s;", t.indent, fieldName, fieldType))
if valueType.Kind() == reflect.Struct {
t.constructorBody = append(t.constructorBody, fmt.Sprintf("%s%sthis%s = this.convertValues(source[\"%s\"], %s, true);",
t.indent, t.indent, dotField, strippedFieldName, t.prefix+valueTypePrefix+valueTypeName+valueTypeSuffix+t.suffix))
Expand Down Expand Up @@ -744,8 +748,9 @@ func (t *TypeScriptify) convertType(depth int, typeOf reflect.Type, customCode m
}
}

builder.AddMapField(jsonFieldName, field)
builder.AddMapField(jsonFieldName, field, t.CreateInterface)
} else if field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Array { // Slice:
nullable := t.CreateInterface && field.Type.Kind() == reflect.Slice
if field.Type.Elem().Kind() == reflect.Ptr { // extract ptr type
field.Type = field.Type.Elem()
}
Expand All @@ -769,10 +774,10 @@ func (t *TypeScriptify) convertType(depth int, typeOf reflect.Type, customCode m
if typeScriptChunk != "" {
result = typeScriptChunk + "\n" + result
}
builder.AddArrayOfStructsField(jsonFieldName, field, arrayDepth)
builder.AddArrayOfStructsField(jsonFieldName, field, arrayDepth, nullable)
} else { // Slice of simple fields:
t.logf(depth, "- slice field %s.%s", typeOf.Name(), field.Name)
err = builder.AddSimpleArrayField(jsonFieldName, field, arrayDepth, fldOpts)
err = builder.AddSimpleArrayField(jsonFieldName, field, arrayDepth, fldOpts, nullable)
}
} else { // Simple field:
t.logf(depth, "- simple field %s.%s", typeOf.Name(), field.Name)
Expand Down Expand Up @@ -848,7 +853,7 @@ type typeScriptClassBuilder struct {
namespace string
}

func (t *typeScriptClassBuilder) AddSimpleArrayField(fieldName string, field reflect.StructField, arrayDepth int, opts TypeOptions) error {
func (t *typeScriptClassBuilder) AddSimpleArrayField(fieldName string, field reflect.StructField, arrayDepth int, opts TypeOptions, nullable bool) error {
fieldType := nameTypeOf(field.Type.Elem())
kind := field.Type.Elem().Kind()
typeScriptType, ok := t.types[kind]
Expand All @@ -859,11 +864,19 @@ func (t *typeScriptClassBuilder) AddSimpleArrayField(fieldName string, field ref
if len(fieldName) > 0 {
strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
if len(opts.TSType) > 0 {
t.addField(fieldName, opts.TSType, false)
fieldType := opts.TSType
if nullable {
fieldType += " | null"
}
t.addField(fieldName, fieldType, false)
t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName))
return nil
} else if len(typeScriptType) > 0 {
t.addField(fieldName, fmt.Sprint(typeScriptType, strings.Repeat("[]", arrayDepth)), false)
fieldType := fmt.Sprint(typeScriptType, strings.Repeat("[]", arrayDepth))
if nullable {
fieldType += " | null"
}
t.addField(fieldName, fieldType, false)
t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("source[\"%s\"]", strippedFieldName))
return nil
}
Expand Down Expand Up @@ -930,13 +943,17 @@ func (t *typeScriptClassBuilder) AddStructField(fieldName string, field reflect.
t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("this.convertValues(source[\"%s\"], %s)", strippedFieldName, classname))
}

func (t *typeScriptClassBuilder) AddArrayOfStructsField(fieldName string, field reflect.StructField, arrayDepth int) {
func (t *typeScriptClassBuilder) AddArrayOfStructsField(fieldName string, field reflect.StructField, arrayDepth int, nullable bool) {
fieldType := nameTypeOf(field.Type.Elem())
if differentNamespaces(t.namespace, field.Type.Elem()) {
fieldType = field.Type.Elem().String()
}
strippedFieldName := strings.ReplaceAll(fieldName, "?", "")
t.addField(fieldName, fmt.Sprint(t.prefix+fieldType+t.suffix, strings.Repeat("[]", arrayDepth)), false)
arrayType := fmt.Sprint(t.prefix+fieldType+t.suffix, strings.Repeat("[]", arrayDepth))
if nullable {
arrayType += " | null"
}
t.addField(fieldName, arrayType, false)
t.addInitializerFieldLine(strippedFieldName, fmt.Sprintf("this.convertValues(source[\"%s\"], %s)", strippedFieldName, t.prefix+fieldType+t.suffix))
}

Expand Down
4 changes: 4 additions & 0 deletions website/src/pages/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Fixed v2 TypeScript interfaces omitting `null` from slice and map field types [#3588](https://github.com/wailsapp/wails/issues/3588) by @Sushanth012

## v2.13.0 - 2026-07-06
### Added

Expand Down
Loading