Skip to content

Commit a8b3dee

Browse files
authored
feat: golang map types unioned with possible 'null' (#18)
* feat: golang map types unioned with possible 'null' Golang `map[<key>]<value>` can be `null` in json. This change unions the Record type with `null`. To revert to the previous behavior, use the mutation 'NotNullMaps'
1 parent 8032d06 commit a8b3dee

File tree

7 files changed

+82
-13
lines changed

7 files changed

+82
-13
lines changed

config/mutations.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,39 @@ func (v *nullUnionVisitor) Visit(node bindings.Node) walk.Visitor {
269269

270270
return v
271271
}
272+
273+
// NotNullMaps assumes all maps will not be null.
274+
// Example:
275+
// GolangType: map[string]string
276+
// TsType: Record<string,string> | null --> Record<string,string>
277+
func NotNullMaps(ts *guts.Typescript) {
278+
ts.ForEach(func(key string, node bindings.Node) {
279+
walk.Walk(&notNullMaps{}, node)
280+
})
281+
}
282+
283+
type notNullMaps struct{}
284+
285+
func (v *notNullMaps) Visit(node bindings.Node) walk.Visitor {
286+
if union, ok := node.(*bindings.UnionType); ok && len(union.Types) == 2 {
287+
hasNull := slices.ContainsFunc(union.Types, func(t bindings.ExpressionType) bool {
288+
_, isNull := t.(*bindings.Null)
289+
return isNull
290+
})
291+
292+
var record bindings.ExpressionType
293+
index := slices.IndexFunc(union.Types, func(t bindings.ExpressionType) bool {
294+
ref, isRef := t.(*bindings.ReferenceType)
295+
if !isRef {
296+
return false
297+
}
298+
return ref.Name.Name == "Record"
299+
})
300+
if hasNull && index != -1 {
301+
record = union.Types[index]
302+
union.Types = []bindings.ExpressionType{record}
303+
}
304+
}
305+
306+
return v
307+
}

convert.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,8 @@ func (ts *Typescript) typescriptType(ty types.Type) (parsedType, error) {
801801
return parsedType{}, xerrors.Errorf("simplify generics in map: %w", err)
802802
}
803803
parsed := parsedType{
804-
Value: RecordReference(keyType.Value, valueType.Value),
804+
// Golang `map` can be marshaled to `null` in json.
805+
Value: bindings.Union(RecordReference(keyType.Value, valueType.Value), &bindings.Null{}),
805806
TypeParameters: tp,
806807
RaisedComments: append(keyType.RaisedComments, valueType.RaisedComments...),
807808
}

convert_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,27 @@ func TestGeneration(t *testing.T) {
117117
})
118118
}
119119
}
120+
121+
func TestNotNullMaps(t *testing.T) {
122+
gen, err := guts.NewGolangParser()
123+
require.NoError(t, err, "new convert")
124+
125+
dir := filepath.Join(".", "testdata", "maps")
126+
err = gen.IncludeGenerate("./" + dir)
127+
require.NoErrorf(t, err, "include %q", dir)
128+
129+
gen.IncludeCustomDeclaration(config.StandardMappings())
130+
131+
ts, err := gen.ToTypescript()
132+
require.NoError(t, err, "to typescript")
133+
134+
ts.ApplyMutations(
135+
config.NotNullMaps,
136+
)
137+
138+
output, err := ts.Serialize()
139+
require.NoErrorf(t, err, "generate %q", dir)
140+
141+
// Not perfect, this asserts if the record is a nullable type.
142+
require.Contains(t, output, "SimpleMap: Record<string, string>;", "no nullable Record")
143+
}

testdata/anyreference/anyreference.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
// From anyreference/anyreference.go
44
export interface Example {
5-
readonly Value: Record<string, string>;
5+
readonly Value: Record<string, string> | null;
66
}
77

88
// From anyreference/anyreference.go

testdata/genericmap/genericmap.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,10 @@ type Custom interface {
1717
Foo | Buzz
1818
}
1919

20-
// Not yet supported
21-
//type FooBuzzMap[R Custom] struct {
22-
// Something map[string]R `json:"something"`
23-
//}
20+
type FooBuzzMap[R Custom] struct {
21+
Something map[string]R `json:"something"`
22+
}
2423

25-
// Not yet supported
26-
//type FooBuzzAnonymousUnion[R Foo | Buzz] struct {
27-
// Something []R `json:"something"`
28-
//}
24+
type FooBuzzAnonymousUnion[R Foo | Buzz] struct {
25+
Something []R `json:"something"`
26+
}

testdata/genericmap/genericmap.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,13 @@ export interface Foo {
1818
export interface FooBuzz<R extends Custom> {
1919
readonly something: readonly R[];
2020
}
21+
22+
// From codersdk/genericmap.go
23+
export interface FooBuzzAnonymousUnion<R extends Foo | Buzz> {
24+
readonly something: readonly R[];
25+
}
26+
27+
// From codersdk/genericmap.go
28+
export interface FooBuzzMap<R extends Custom> {
29+
readonly something: Record<string, R> | null;
30+
}

testdata/maps/maps.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
// From maps/map.go
44
export interface Bar<T extends any> {
5-
readonly SimpleMap: Record<string, string>;
6-
readonly NumberMap: Record<string, number>;
7-
readonly GenericMap: Record<string, T>;
5+
readonly SimpleMap: Record<string, string> | null;
6+
readonly NumberMap: Record<string, number> | null;
7+
readonly GenericMap: Record<string, T> | null;
88
}

0 commit comments

Comments
 (0)