Decorators with ES7

Exploring Decorators and Bindings

Jun 24, 2015 · 3 min read

Next ES7 version has some experimental functionality that can be explored today using babel. Among other functionality, we have access to an experimental feature called decorators.

What's a Decorator?

From the decorator proposal repository:

Decorators make it possible to annotate and modify classes and properties at design time.

Decorators restore the ability to run code at design time, while maintaining a declarative syntax.

Basically, we can define a decorator for a class or a property:

class decorator

function testable (target) {
    target.isTestable = true
}

@testable
class MyTestableClass () {}

console.log(MyTestableClass.isTestable) // true

If we want to pass parameters we can define the decorator function as a factory:

function testable(isTestable) {
    return function(target) {
        target.isTestable = isTestable
    }
}

@testable(true) class MyTestableClass () {}
console.log(MyTestableClass.isTestable) // true

@testable(false) class MyClass () {}
console.log(MyClass.isTestable) // false

You can read more from the proposal about how to define property decorators.

Applying Mixins Declaratively

One of the things I really like is to use Mixins (actually not mixins but traits, but we will talk about it later in this post). In ES5 we can merge prototypes using an Object.assign polyfill, underscore or lodash (_.extend):

var assign = require('object.assign');

function Foo () {};
Foo.prototype.foo = function() { console.log('foo'); };

function MyClass () {}
assign(MyClass.prototype, Foo.prototype);

var obj = new MyClass();
obj.foo(); // 'foo'

In ES6 we cannot simply use a prototype as a mixin. When defining a class using the new syntax, all methods are not enummerable by default. Object.assign will only merge the enummerable methods from the given objects.

We can use Objects as mixins and then apply them using Object.assign:

const Foo = {
    foo() { console.log('foo') }
}

class MyClass {}

Object.assign(MyClass.prototype, Foo)

let obj = new MyClass()
obj.foo() // 'foo'

This is Imperative style. Now, what if we can do the merge right when we are declaring the class? Let's create a simple decorator to help us here:

mixins.js


export function mixins(...list) {
    return function (target) {
        Object.assign(target.prototype, ...list)
    }
}

Now we can import the new module and define our class declaratively

import { mixins } from './mixins'

const Foo = {
    foo() { console.log('foo') }
}

@mixins(Foo)
class MyClass {}

let obj = new MyClass()

obj.foo() // 'foo'

Update: I run accross this npm module mixins-decorator which already provides this functionality, plus, it makes class defined methods to take precedence over mixins (which is better than the simplest solution presented here).

Traits

I mentioned before I really like the idea of Traits. While mixins are ok, there are sometimes when you need a bit more control on what you want to merge. Traits allow us to avoid any name collision between the functionality that is being merged. We can exclude methods from a given Trait, or change the name of the method using aliases.

The only thing that is required is that a Trait does not define state.

The good part is we can use ES6 classes as Traits.

I have been working on CocktailJS, a library to define annotations, traits and classes in a more declarative way. So, with all this new functionality as decorators, I wanted to review the library in itself and see if we can do something simpler.

The answer is yes, of course we can! Check traits-decorator! This is an experimental library to use decorators and bind-operators.


import {traits } from 'traits-decorator'

// Trait as a Class
class TFoo {
    foo() { console.log('foo') }
}

// Trait as an Object
const TBar = {
    bar() { console.log('bar') }
}

@traits(TFoo, TBar)
class MyClass { }

let obj = new MyClass()
obj.foo() // foo
obj.bar() // bar

Conflicts

We will have an error if any of the Traits or even the host class have a name collision:

name collision on method foo


import {traits } from 'traits-decorator'

// Trait as a Class
class TFoo {
    foo() { console.log('foo') }
}

// Trait as an Object
const TBar = {
    bar() { console.log('bar') },
    foo() { console.log('uuups') }
}

@traits(TFoo, TBar)
class MyClass { }
//....
index.js:36
    throw new Error('Method named: ' + methodName + ' is defined twice.');
          ^
Error: Method named: foo is defined twice.
...

This will give you an error since we have the method foo defined twice in TFoo and TBar. Traits makes the developer responsible to resolve this conflict in opposite to mixins where TFoo's foo will be overriden by TBar's foo.

To solve the conflict we can exclude the foo method we don't need or create an alias to change the name:

solving conflict by excluding the method


import { traits, excludes } from 'traits-decorator'

// Trait as a Class
class TFoo {
    foo() { console.log('foo') }
}

// Trait as an Object so we can reuse our mixins with more control!
const TBar = {
    bar() { console.log('bar') },
    foo() { console.log('uuups') }
}

@traits(TFoo, TBar::excludes('foo'))
class MyClass { }

let obj = new MyClass()
obj.foo() // foo
obj.bar() // bar

You will notice a little thing here: we are using :: the bind-operator in the traits definition.

Basically the bind operator allow us to execute a given method as it were part of the object, in this case the Trait class or object.

In order to use these particular functionality we need to enable stage 0 from babel experimental features.

Final Words

I really like the idea of decorators and bind operator. I think they are pretty helpful specially to write cleaner and more maintainable code.

Any viewpoints and opinions expressed in this article are my own and do not, in any way, reflect those of my employer, my colleagues, or anyone else. I speak only for myself, not for them.