Description
wails3 generate bindings -ts can emit a models module that throws the moment it is imported:
ReferenceError: Cannot access '$$createType1' before initialization
This is a runtime crash on first import, not a type error — so it white-screens the app at boot and fails every test suite that touches the bindings, while tsc stays perfectly happy.
Reproduction
Self-contained repro (5 files): https://gist.github.com/hamzabisevac/ce2a1605e949c925ac6293068a08e8d3
Two models are enough. From an empty directory:
go mod init example.com/tdzrepro
go get github.com/wailsapp/wails/v3@v3.0.0-beta.16
# write main.go (below)
wails3 generate bindings -ts -d out
main.go:
package main
import "github.com/wailsapp/wails/v3/pkg/application"
// Alpha sorts before Zeta, so the generator emits it first.
type Alpha struct {
Name string
Box Zeta[string]
}
// Zeta is generic; []T and *T each need an element converter.
type Zeta[T any] struct {
Items []T
Ref *T
}
type Service struct{}
func (s *Service) Get() Alpha { return Alpha{} }
func main() {
app := application.New(application.Options{
Services: []application.Service{application.NewService(&Service{})},
})
_ = app
}
The generated out/example.com/tdzrepro/models.ts ends with:
// Private type creation functions
const $$createType0 = Zeta.createFrom($Create.Any);
const $$createType1 = ($$createParamT: any) => $Create.Array($$createParamT);
const $$createType2 = ($$createParamT: any) => $Create.Nullable($$createParamT);
To see it throw:
npm i @wailsio/runtime@3.0.0-beta.16 # package.json with "type": "module"
node --experimental-strip-types -e "import('./out/example.com/tdzrepro/models.ts')"
ReferenceError: Cannot access '$$createType1' before initialization
Cause
createFrom on a generic class resolves its field converters eagerly, when it is called, rather than deferring them into the closure it returns:
static createFrom<T = any>($$createParamT: (source: any) => T): ($$source?: any) => Zeta<T> {
const $$createField0_0 = $$createType1($$createParamT); // ← evaluated now
const $$createField1_0 = $$createType2($$createParamT); // ← evaluated now
return ($$source: any = {}) => { /* ... */ };
}
$$createType0 invokes exactly that at module scope. The private-type block is emitted in creation order, so when a holder type is emitted before the generic type it instantiates, the eager call receives a lower index than the generic class's own helper arrows — and reads them while they are still in the temporal dead zone.
The ordering is the whole bug
Rename Alpha → Holder and Zeta → Box in main.go, change nothing else, and regenerate. The generic type is now emitted first, its helpers land above the eager call, and the identical model graph imports cleanly:
const $$createType0 = ($$createParamT: any) => $Create.Array($$createParamT);
const $$createType1 = ($$createParamT: any) => $Create.Nullable($$createParamT);
const $$createType2 = Box.createFrom($Create.Any);
Same structure, only the names differ. That sensitivity is why this is easy to miss — and why, on a real model graph, it looks like the generator randomly breaks.
Suggested fixes
Any one of:
- Emit the
($$createParamT) => ... helper arrows ahead of every eager X.createFrom(...) in the private-type block. Smallest change — those arrows close over nothing but $Create and their own parameter, so reordering them is semantically inert.
- Defer field-converter resolution into the closure returned by a generic
createFrom.
- Extend the existing lazy-init escape hatch (
$$initCreateTypeN, internal/generator/render/create.go) to generic classes. Today it is applied only to non-class named types, so generic classes take the eager path.
Environment
|
|
| Wails |
v3.0.0-beta.16 |
| @wailsio/runtime |
3.0.0-beta.16 |
| Go |
go1.26.5 |
| OS |
Windows 11 — but this is code generation, so it should be platform-independent |
Description
wails3 generate bindings -tscan emit a models module that throws the moment it is imported:This is a runtime crash on first import, not a type error — so it white-screens the app at boot and fails every test suite that touches the bindings, while
tscstays perfectly happy.Reproduction
Self-contained repro (5 files): https://gist.github.com/hamzabisevac/ce2a1605e949c925ac6293068a08e8d3
Two models are enough. From an empty directory:
go mod init example.com/tdzrepro go get github.com/wailsapp/wails/v3@v3.0.0-beta.16 # write main.go (below) wails3 generate bindings -ts -d outmain.go:The generated
out/example.com/tdzrepro/models.tsends with:To see it throw:
Cause
createFromon a generic class resolves its field converters eagerly, when it is called, rather than deferring them into the closure it returns:$$createType0invokes exactly that at module scope. The private-type block is emitted in creation order, so when a holder type is emitted before the generic type it instantiates, the eager call receives a lower index than the generic class's own helper arrows — and reads them while they are still in the temporal dead zone.The ordering is the whole bug
Rename
Alpha→HolderandZeta→Boxinmain.go, change nothing else, and regenerate. The generic type is now emitted first, its helpers land above the eager call, and the identical model graph imports cleanly:Same structure, only the names differ. That sensitivity is why this is easy to miss — and why, on a real model graph, it looks like the generator randomly breaks.
Suggested fixes
Any one of:
($$createParamT) => ...helper arrows ahead of every eagerX.createFrom(...)in the private-type block. Smallest change — those arrows close over nothing but$Createand their own parameter, so reordering them is semantically inert.createFrom.$$initCreateTypeN,internal/generator/render/create.go) to generic classes. Today it is applied only to non-class named types, so generic classes take the eager path.Environment