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:
Instantiating the class directly. E.g. new Foo().
Making derived classes. E.g. class Bar extends Foo {}.
Using Foo in generic functions that require a constructor.
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.
Motivation
A simple empty struct was previously typed as follows:
This typing is incorrect. In JavaScript, a missing constructor is equivalent to an empty constructor.
Example:
So the above type for
Foo
is actually just a short form for this: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 aboveFoo
struct would get this type definitions: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:
new Foo()
.class Bar extends Foo {}
.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.