Skip to content

Commit 0505c1c

Browse files
committed
fix: make reflect.Type.Implements() required and add typed nil support
Implement proper typed nil pointer handling for type conversions like (*Interface)(nil) and make reflect.Type.Implements() a required method across all type implementations. This fixes TypeScript type errors and enables correct interface implementation checking via reflection. Previously, Implements() was marked as optional (Implements?), causing TypeScript to emit errors when calling it. Additionally, nil pointer conversions didn't preserve type information needed for reflection. Changes: - Made Implements() required in Type interface and added null checks - Added typedNil() function to create typed nil pointers with metadata - Added compareTypeStringWithTypeInfo() for type comparison - Added missing Implements() method to MapType - Added compliance tests for reflect.Implements, typed nil, and interface subsets Signed-off-by: Christian Stewart <christian@aperture.us>
1 parent 2dae877 commit 0505c1c

26 files changed

+918
-41
lines changed

compiler/expr-call-type-conversion.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,25 @@ func (c *GoToTSCompiler) writeNilConversion(exp *ast.CallExpr) (handled bool, er
1818
return false, nil
1919
}
2020

21-
// Handle nil pointer to struct type conversions: (*struct{})(nil)
22-
if starExpr, isStarExpr := exp.Fun.(*ast.StarExpr); isStarExpr {
23-
if _, isStructType := starExpr.X.(*ast.StructType); isStructType {
24-
c.tsw.WriteLiterally("null")
21+
// Get the type being converted to
22+
if typ := c.pkg.TypesInfo.TypeOf(exp.Fun); typ != nil {
23+
// For pointer types, create a typed nil that preserves type information
24+
if ptrType, ok := typ.(*types.Pointer); ok {
25+
// Use a qualifier that returns the package name for local types
26+
// This matches Go's reflect output format (e.g., "main.Stringer")
27+
qualifier := func(pkg *types.Package) string {
28+
if pkg == nil {
29+
return ""
30+
}
31+
return pkg.Name()
32+
}
33+
typeName := types.TypeString(ptrType, qualifier)
34+
c.tsw.WriteLiterallyf("$.typedNil(%q)", typeName)
2535
return true, nil
2636
}
2737
}
2838

39+
// For non-pointer types (or if type info is unavailable), use plain null
2940
c.tsw.WriteLiterally("null")
3041
return true, nil
3142
}

compliance/WIP.md

Lines changed: 0 additions & 34 deletions
This file was deleted.

compliance/tests/inline_interface_type_assertion/inline_interface_type_assertion.gs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export async function main(): Promise<void> {
127127
}
128128

129129
// Test case: nil value of an inline interface type assigned to interface{}
130-
let l: null | any = null
130+
let l: null | any = $.typedNil("*struct{Name string}")
131131

132132
let { value: ptr, ok: ok6 } = $.typeAssert<{ Name?: string } | null>(l, {kind: $.TypeKind.Pointer, elemType: {kind: $.TypeKind.Struct, fields: {'Name': {kind: $.TypeKind.Basic, name: 'string'}}, methods: []}})
133133
if (ok6) {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
i1.MyString1(): hello
2+
i1.MyString2(): world
3+
i2.MyString1(): hello
4+
Type assertion successful
5+
i3.MyString1(): hello
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { MyStruct } from "./interface_subset_cast.gs.js"
2+
export type { MyInterface1, MyInterface2 } from "./interface_subset_cast.gs.js"
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package main
2+
3+
type MyInterface1 interface {
4+
MyString1() string
5+
MyString2() string
6+
}
7+
8+
type MyInterface2 interface {
9+
MyString1() string
10+
}
11+
12+
type MyStruct struct {
13+
Value1 string
14+
Value2 string
15+
}
16+
17+
func (m MyStruct) MyString1() string {
18+
return m.Value1
19+
}
20+
21+
func (m MyStruct) MyString2() string {
22+
return m.Value2
23+
}
24+
25+
func main() {
26+
s := MyStruct{Value1: "hello", Value2: "world"}
27+
var i1 MyInterface1 = s
28+
29+
// Cast from larger interface to smaller interface (subset)
30+
var i2 MyInterface2 = i1
31+
32+
println("i1.MyString1():", i1.MyString1())
33+
println("i1.MyString2():", i1.MyString2())
34+
println("i2.MyString1():", i2.MyString1())
35+
36+
// Type assertion from larger to smaller interface
37+
i3, ok := i1.(MyInterface2)
38+
if ok {
39+
println("Type assertion successful")
40+
println("i3.MyString1():", i3.MyString1())
41+
} else {
42+
println("Type assertion failed")
43+
}
44+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Generated file based on interface_subset_cast.go
2+
// Updated when compliance tests are re-run, DO NOT EDIT!
3+
4+
import * as $ from "@goscript/builtin/index.js"
5+
6+
export type MyInterface1 = null | {
7+
MyString1(): string
8+
MyString2(): string
9+
}
10+
11+
$.registerInterfaceType(
12+
'main.MyInterface1',
13+
null, // Zero value for interface is null
14+
[{ name: "MyString1", args: [], returns: [{ type: { kind: $.TypeKind.Basic, name: "string" } }] }, { name: "MyString2", args: [], returns: [{ type: { kind: $.TypeKind.Basic, name: "string" } }] }]
15+
);
16+
17+
export type MyInterface2 = null | {
18+
MyString1(): string
19+
}
20+
21+
$.registerInterfaceType(
22+
'main.MyInterface2',
23+
null, // Zero value for interface is null
24+
[{ name: "MyString1", args: [], returns: [{ type: { kind: $.TypeKind.Basic, name: "string" } }] }]
25+
);
26+
27+
export class MyStruct {
28+
public get Value1(): string {
29+
return this._fields.Value1.value
30+
}
31+
public set Value1(value: string) {
32+
this._fields.Value1.value = value
33+
}
34+
35+
public get Value2(): string {
36+
return this._fields.Value2.value
37+
}
38+
public set Value2(value: string) {
39+
this._fields.Value2.value = value
40+
}
41+
42+
public _fields: {
43+
Value1: $.VarRef<string>;
44+
Value2: $.VarRef<string>;
45+
}
46+
47+
constructor(init?: Partial<{Value1?: string, Value2?: string}>) {
48+
this._fields = {
49+
Value1: $.varRef(init?.Value1 ?? ""),
50+
Value2: $.varRef(init?.Value2 ?? "")
51+
}
52+
}
53+
54+
public clone(): MyStruct {
55+
const cloned = new MyStruct()
56+
cloned._fields = {
57+
Value1: $.varRef(this._fields.Value1.value),
58+
Value2: $.varRef(this._fields.Value2.value)
59+
}
60+
return cloned
61+
}
62+
63+
public MyString1(): string {
64+
const m = this
65+
return m.Value1
66+
}
67+
68+
public MyString2(): string {
69+
const m = this
70+
return m.Value2
71+
}
72+
73+
// Register this type with the runtime type system
74+
static __typeInfo = $.registerStructType(
75+
'main.MyStruct',
76+
new MyStruct(),
77+
[{ name: "MyString1", args: [], returns: [{ type: { kind: $.TypeKind.Basic, name: "string" } }] }, { name: "MyString2", args: [], returns: [{ type: { kind: $.TypeKind.Basic, name: "string" } }] }],
78+
MyStruct,
79+
{"Value1": { kind: $.TypeKind.Basic, name: "string" }, "Value2": { kind: $.TypeKind.Basic, name: "string" }}
80+
);
81+
}
82+
83+
export async function main(): Promise<void> {
84+
let s = $.markAsStructValue(new MyStruct({Value1: "hello", Value2: "world"}))
85+
let i1: MyInterface1 = $.markAsStructValue(s.clone())
86+
87+
// Cast from larger interface to smaller interface (subset)
88+
let i2: MyInterface2 = i1
89+
90+
console.log("i1.MyString1():", i1!.MyString1())
91+
console.log("i1.MyString2():", i1!.MyString2())
92+
console.log("i2.MyString1():", i2!.MyString1())
93+
94+
// Type assertion from larger to smaller interface
95+
let { value: i3, ok: ok } = $.typeAssert<MyInterface2>(i1, 'main.MyInterface2')
96+
if (ok) {
97+
console.log("Type assertion successful")
98+
console.log("i3.MyString1():", i3!.MyString1())
99+
}
100+
else {
101+
console.log("Type assertion failed")
102+
}
103+
}
104+
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"compilerOptions": {
3+
"allowImportingTsExtensions": true,
4+
"lib": [
5+
"es2022",
6+
"esnext.disposable",
7+
"dom"
8+
],
9+
"module": "nodenext",
10+
"moduleResolution": "nodenext",
11+
"noEmit": true,
12+
"paths": {
13+
"*": [
14+
"./*"
15+
],
16+
"@goscript/*": [
17+
"../../../gs/*",
18+
"../../../compliance/deps/*"
19+
],
20+
"@goscript/github.com/aperturerobotics/goscript/compliance/tests/interface_subset_cast/*": [
21+
"./*"
22+
]
23+
},
24+
"sourceMap": true,
25+
"target": "es2022"
26+
},
27+
"extends": "../../../tsconfig.json",
28+
"include": [
29+
"index.ts",
30+
"interface_subset_cast.gs.ts"
31+
]
32+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
MyInterface1: hello world
2+
MyInterface1: hello world
3+
MyInterface1: hello world
4+
Matched MyInterface2 from i1: hello
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { MyStruct } from "./interface_subset_type_switch.gs.js"
2+
export type { MyInterface1, MyInterface2 } from "./interface_subset_type_switch.gs.js"

0 commit comments

Comments
 (0)