rustwasm / wasm-bindgen

Facilitating high-level interactions between Wasm modules and JavaScript
https://rustwasm.github.io/docs/wasm-bindgen/
Apache License 2.0
7.83k stars 1.08k forks source link

Use private constructors in type definitions of structs without an exported constructor #4282

Open RunDevelopment opened 3 days ago

RunDevelopment commented 3 days ago

Motivation

A simple empty struct was previously typed as follows:

#[wasm_bindgen]
struct Foo;
export class Foo {
  free(): void;
}

This typing is incorrect. In JavaScript, a missing constructor is equivalent to an empty constructor.

Example:

// omitting the constructor...
class Example { }

// ... is the same as this
class Example {
  constructor() {}
}

So the above type for Foo is actually just a short form for this:

export class Foo {
  constructor();
  free(): void;
}

Obviously, this is not correct. The Foo class is not supported to be instantiate-able using the class constructor. In debug mode, WBG even generates a constructor that always throws.

Changes in this PR

This PR solves this problem by adding a private constructor for structs with a #[wasm_bindgen(constructor)]. E.g. the above Foo struct would get this type definitions:

export class Foo {
  private constructor();
  free(): void;
}

A private constructor in TS declares the constructor as inaccessible to anyone besides the class itself. In particular, a private constructor prevents users in TypeScript from:

  1. Instantiating the class directly. E.g. new Foo().
  2. Making derived classes. E.g. class Bar extends Foo {}.
  3. Using Foo in generic functions that require a constructor.

See this TS playground for proof.

With those changes, the type definitions now accurately represent the correct usage of class Foo and correctly cause errors on incorrect usage.

Is this a breaking change?

Probably not.

Since this is a type-only change, nothing will break at runtime (more than it already is). Users using the implicitly defined constructor most likely isn't intended by WBG since WBG even generates a constructor that always throws in debug mode to prevent exactly this.

So this PR will at most cause new TS compiler errors for incorrect code. From that perspective, I would argue that this PR is not a breaking change.

RunDevelopment commented 2 days ago

So that's cool. Turns out, 2 of our TS tests used the unintended default constructors.