-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Mixins
Mixins have a very low level support in Closure Compiler's type system. It's possible to annotate code so that mixins can be typechecked, but requires some extra boilerplate. Closure Compiler does not understand mixins at all in unannotated code.
While there are a wide variety of different mixin patterns in JavaScript, this page will discuss how to implement mixins using inheritance.
Say you have a mixin that adds a hello
method. This may be implemented (omitting any Closure types for now) as.
const helloMixin = superClass => class extends superClass {
hello(name) {
console.log(`Hello, ${name}.`);
}
};
Let's also define a sample usage of this mixin, and intentionally insert some bugs we'd like Closure Compiler to report.
class GoodbyeSayer {
goodbye(name) {
console.log(`Good bye, ${name}.`);
}
}
const SocialButterfly = helloMixin(GoodbyeSayer);
const butterfly = new SocialButterfly();
// Oops - we forgot that both hello and goodbye expect a name.
butterfly.hello(); // "Hello, undefined."
butterfly.goodbye(); // "Good bye, undefined."
Given this code, Closure Compiler has no idea what the type of SocialButterfly
is. This code won't cause any type errors or missing property warnings, but also won't emit the useful error saying that hello
and goodbye
are called with too few arguments. (If you're enabling https://github.com/google/closure-compiler/wiki/JS-Conformance-Framework, this will trigger certain unknown property type errors).
Getting Closure Compiler to type your mixin requires creating a new interface containing all the properties in the mixin.
This is necessary because Closure Compiler currently does not support multiple inheritance or inferring intersection types. The closest feature in the type system is @implements
.
In order to get Closure to know what types you are returning from helloMixin
, you must
- define an @interface
HelloMixin
that contains all the properties and methods added by your mixin - annotate the return value of
helloMixin
with@implements {HelloMixin}
- annotate the return value of
helloMixin(GoodbyeSayer)
with@implements {Mixin}
- annotate the return value of
helloMixin(GoodbyeSayer)
with@constructor @extends {GoodbyeSayer}
Step 2 just ensures that your interface matches your actual mixin implementation. It doesn't actually affect type inference on any calls to the mixin.
Step 3 tells the compiler that the return value contains the properties defined in your mixin.
Step 4 tells the compiler that the return value is a subclass of GoodbyeSayer.
Here's what the end result should look like:
/** @interface */
class HelloMixin {
hello(name) {}
}
const helloMixin = superClass =>
/** @implements {HelloMixin} */
class extends superClass {
hello(name) {
console.log(`Hello, ${name}.`);
}
};
class GoodbyeSayer {
goodbye(name) {
console.log(`Good bye, ${name}.`);
}
}
/**
* @constructor
* @implements {HelloMixin}
* @extends {GoodbyeSayer}
*/
const SocialButterfly = helloMixin(GoodbyeSayer);
const butterfly = new SocialButterfly();
butterfly.hello();
butterfly.goodbye();
Now Closure Compiler is able to recognize the types of both hello
and goodbye
and emits the following warnings:
input0:29: WARNING - [JSC_WRONG_ARGUMENT_COUNT] Function SocialButterfly.prototype.hello: called with 0 argument(s). Function requires at least 1 argument(s) and no more than 1 argument(s).
butterfly.hello();
^^^^^^^^^^^^^^^^^
input0:30: WARNING - [JSC_WRONG_ARGUMENT_COUNT] Function GoodbyeSayer.prototype.goodbye: called with 0 argument(s). Function requires at least 1 argument(s) and no more than 1 argument(s).
butterfly.goodbye();
^^^^^^^^^^^^^^^^^^^
The previous step skipped adding @param
and @return
to helloMixin
. This is because there is no way to sufficiently express the result of helloMixin
with these annotations alone. However, it's a good idea to annotate the actual mixin to catch a few more bugs. Let's express that:
- the mixin must be called with a var-args function. (We'd like to restrict this further to constructors, but that's not currently possible in JSDoc.)
- the mixin is known to return a subclass of the constructor passed as a parameter
For now, let's type both the input and output constructors as taking any number of arguments of any type.
/**
* @param {function(new: T, ...?): ?} superClass
* @return {function(new: T, ...?): ?}
* @template T
*/
const helloMixin = superClass =>
/** @implements {HelloMixin} */
class extends superClass {
helloThere(name) {
console.log(`Hello, ${name}.`);
}
};
Unfortunately there's no way to get Closure Compiler to infer the constructor parameters based on the base class. The only options are
- allow var args parameters with
...?
, as in the above example - hard-code the expected parameters of the constructor (e.g. replace
...?
withstring, number = undefined
)
- requires an @interface definition separate from the actual mixin
- cannot infer types of constructor parameters automatically