Skip to content

[v3] Generated TS bindings throw "Cannot access '$$createTypeN' before initialization" when a generic model is emitted after its holder #6062

Description

@hamzabisevac

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 AlphaHolder and ZetaBox 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);
IMPORT OK

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:

  1. 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.
  2. Defer field-converter resolution into the closure returned by a generic createFrom.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions