mics
mics copied to clipboard
Multiple Inheritance Class System: Intuitive mixins for ES6 classes
mics 0.7.0
Multiple Inheritance Class System
Intuitive mixins for ES6 classes

Multiple Inheritance is like a parachute. You don't often need it, but when you do, you really need it. Grady Booch
What is it
mics (pronounce: mix) is a library that makes multiple inheritance in Javascript a breeze. Inspired by the excellent blog post "Real" Mixins with Javascript Classes by Justin Fagnani, mics tries to build a minimal library around the concept of using class expressions (factories) as mixins. mics extends the concepts presented in the blog post by making the mixins first-class citizens that can be directly used to instantiate objects and can be mixed in with other mixins instead of just with classes.
Install with NPM
npm install --save mics
Direct download
- mics.umd.js (universal module works in browser and node)
- mics.min.js (minified version of universal module file)
Include in your app
import
import { mix, is, like } from 'mics'
require
var mix = require('mics').mix
var is = require('mics').is
var like = require('mics').like
AMD
define(['mics'], function(mics){
var mix = mics.mix
var is = mics.is
var like = mics.like
});
Script tag
<script src="https://cdn.rawgit.com/download/mics/0.7.0/dist/mics.min.js"></script>
<script>
var mix = mics.mix
var is = mics.is
var like = mics.like
</script>
Usage
Creating a mixin

Mixins are like classes on steroids. They look and feel a lot like ES6 classes, but they have some additional capabilities that ES6 classes do not have:
- They can 'extend' from multiple other mixins including (at most one) ES6 class
- They have an explicit
interfacewhich can be inspected and tested at runtime - They have an ES6
classthat is used to create instances - They have a
mixinfunction that mixes in their class body into another type. - They can be invoked without
newto create new instances
mixin: An ES5 constructor function that has properties
mixin,classandinterface.
You create mixins with the mix function.
mix([superclass] [, ...mixins] [, factory])
mix accepts an optional superclass as the first argument, then a bunch of mixins
and an optional class factory as the last argument and returns a mixin.
Mostly, you will be using mix with a factory to create mixins, like this:
import { mix, is, like } from 'mics'
var Looker = mix(superclass => class Looker extends superclass {
constructor() {
super()
console.info('A looker is born!')
}
look() {
console.info('Looking good!')
}
})
typeof Looker // 'function'
typeof Looker.mixin // 'function'
typeof Looker.class // 'function'
typeof Looker.interface // 'object'
Notice that the argument to mix is an arrow function that accepts a superclass and
returns a class that extends the given superclass. The body of the mixin is defined in
the returned class. We call this a class factory.
Class factory: An arrow function that accepts a
superclassand returns aclass extends superclass.
The mix function creates a mixing function based on the given mixins and the class
factory and invokes it with the given superclass to create the ES6 class backing the mixin.
It then creates an ES5 constructor function that uses the ES6 class to create and return
new instances of the mixin. Finally it constructs the mixin's interface from the class
prototype and attaches the mixin function, the class and the interface to the ES5
constructor function, creating what in the context of mics we call a mixin.
Creating instances of mixins
We can directly use the created mixin to create instances, because it is just a constructor function:
var looker = new Looker() // > A looker is born!
looker.look() // > Looking good!
looker instanceof Looker // true
And because it's an ES5 constructor function, we are allowed to invoke it without new:
var looker = Looker() // > A looker is born!
looker.look() // > Looking good!
ES6 made newless invocation of constructors throw an error for ES6 classes, because in ES5 it was often a cause for bugs when programmers forgot
newwith constructors that assumednewwas used. However I (with many others) believe that not usingnewis actually better for writing maintainable code. So mics makes sure that it's constructors work whether you usenewon them or not, because the backing ES6 class is always invoked withnewas it should be. Whether you want to writenewor not in your code is up to you.
Mixing multiple mixins into a new mixin
Let us define mixins Walker and Talker to supplement our Looker:
var Walker = mix(superclass => class Walker extends superclass {
walk() {
console.info('Step, step, step')
}
})
var Talker = mix(superclass => class Talker extends superclass{
talk(){
console.info('Blah, blah, blah')
}
})
Now that we have a bunch of mixins, we can start to use them to achieve multiple inheritance:
var Duck = mix(Looker, Walker, Talker, superclass => class Duck extends superclass {
talk() {
var org = super.talk()
console.info('Quack, quack, quack (Duckian for "' + org + '")')
}
})
var donald = Duck()
donald.talk() // > Quack, quack, quack (Duckian for "Blah, blah, blah")
As you can see, we can override methods and use super to call the superclass method,
just like we can with normal ES6 classes.
Testing if an object is (like) a mixin or class
instanceof works for mixin instances like it does for ES6 classes. But, like ES6
classes, it does not support multiple inheritance. In the example above, Looker
is effectively the superclass for Duck. Walker and Talker are mixed into Duck
by dynamically creating new classes and injecting them into the inheritance chain
between Looker and Duck. Because these are new classes, instances of them are
not recognized by instanceof as instances of Walker and Talker.
Fortunately, mics gives us an is function, which does understand multiple inheritance.
is(subject, type)
Tests whether subject is-a type or extends from type.
The first parameter to is defines the subject to test. This can be an instance or
a type. The second parameter is either a type (constructor function, ES6 class or mixin)
or a type string.
duck instanceof Duck // true
duck instanceof Looker // true, but:
duck instanceof Walker // false! mix created a *new class* based on the factory
// `is` to the rescue!
is(duck, Walker) // true
// we can also test the type
is(Duck, Walker) // true
is(Talker, Walker) // false
like(subject, type)
Often, we don't really care whether the object is a certain type, we just want to know
whether we can treat it like a certain type. Use like(subject, type) to test whether
a subject adheres to the same interface as is defined by type:
var viewer = { // create an object with the
look(){} // same interface as Looker
}
is(viewer, Looker) // false, but
like(viewer, Looker) // true
A good example of how this might be useful can be found in the new ES6 feature Promises.
Here we have the concept of a 'thenable'. This is any object that has a then method on
it. Methods in the Promise API often accept thenables instead of promise instances. Have
a look at Promise.resolve for example:
Promise.resolve(value) Returns a Promise object that is resolved with the given value. If the value is a thenable (i.e. has a
thenmethod), the returned promise will "follow" that thenable, adopting its eventual state; otherwise the returned promise will be fulfilled with the value. mdn
Using mix to define an interface and like to test for it, we can very naturally
express the concept of a thenable from the Promise spec in code:
/** Defines a Thenable */
var Thenable = mix(superclass => class Thenable extends superclass {
then() {}
})
/** Some mixin which can be treated as a Thenable */
var MyPromise = mix(superclass => class MyPromise extends superclass {
then(resolve, reject) {
resolve('Hello, World!')
}
}
// We can check whether the class is thenable using like
like(MyPromise, Thenable) // true
// we can also check instances
var promise = new MyPromise()
like(promise, Thenable) // true
// Ok, that means we can use Promise.resolve!
Promise.resolve(promise).then((result) => {
console.info(result) // > 'Hello, World!'
})
Using a custom ES5 constructor
The default constructor returned from mix is a one-liner that invokes the ES6
class with new. But there could be reasons to use a different function instead.
mix allows you to supply a custom constructor to be used instead. You do this
by providing a static constructor in the class body:
var Custom = mix(superclass => class Custom extends superclass{
static constructor(...args){
console.info('Custom constructor called!')
return new this(...args)
}
})
var test = Custom() // > 'Custom constructor called!'
is(test, Custom) // true
Bonus
As a bonus, you can use is() to do some simple type tests by passing a
string for the type:
class X {}
var factory = superclass => class Y extends superclass {}
var Y = mix(factory)
var Z = mix(X, Y)
is(X, 'function') // true
is(X, 'class') // true
is(X, 'mixin') // false
is(X, 'factory') // false
is(factory, 'function') // true
is(factory, 'class') // false
is(factory, 'mixin') // false
is(factory, 'factory') // true
is(Y, 'function') // true
is(Y, 'class') // false
is(Y, 'mixin') // true
is(Y, 'factory') // false
is(Z, 'function') // true
is(Z, 'class') // false
is(Z, 'mixin') // true
is(Z, 'factory') // false
Supported type strings: "class", "mixin", "factory", and any type strings
that can be passed to typeof.
- class: x is a (possibly Babel-transpiled) ES6 class
- mixin: x is a mixin that is the result of calling
mix - factory: x is a class factory function
Issues
Add an issue in this project's issue tracker to let me know of any problems you find, or questions you may have.
Credits
Credits go to Justin Fagnani for his excellent blog post "Real" Mixins with JavaScript Classes and the accompanying library mixwith.js.
Contributors
Many thanks to Marco Alka for his contributions to this project.
Copyright
Copyright 2017 by Stijn de Witt and contributors. Some rights reserved.
License
Licensed under the Creative Commons Attribution 4.0 International (CC-BY-4.0) Open Source license.