Decorate Your Javascript

Javascript decorators are a form of metaprogramming: they add functionality to classes and properties. Unlike the GoF pattern, where decorators modify instances of a class, Javascript decorators are run when the class, method, or property is installed, modifying all instances.

Decorators are useful for adding extra functionality to behaviours and properties that would otherwise look like boilerplate — such as cacheing, access control, logging, or instrumentation.

How does it work?

A decorator function runs before the object it decorates is installed on the prototype. When you define an undecorated object like this:

1
2
3
4
5
class ExampleWithoutDecoration {
  doWork() {
    console.log('can\'t you see I\'m working here?')
  }
}

The Javascript engine creates an object and installs the doWork method on its prototype:

1
2
3
4
5
6
Object.defineProperty(ExampleWithoutDecoration.prototype, 'doWork', {
  value: specifiedFunction,
  enumerable: false,
  configurable: true,
  writable: true
})

When you define a DECORATED method like this:

1
2
3
4
5
6
7
class ExampleWithDecoration {

  @DecoratingIsFun
  doWork() {
    console.log('can\'t you see I\'m working here?')
  }
}

The Javascript engine saves some temporary state, runs the decorator function, and then installs the doWork method on the object’s prototype.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let methodDescription = {
  type: 'method',
  initializer: () => specifiedFunction,
  enumerable: false,
  configurable: true,
  writable: true
};

methodDescription = DecoratingIsFun(ExampleWithDecoration.prototype, 'doWork', methodDescription) || methodDescription

defineDecoratedProperty(ExampleWithDecoration.prototype, 'doWork', methodDescription);

function defineDecoratedProperty(target, { initializer, enumerable, configurable, writable }) {
  Object.defineProperty(target, { value: initializer(), enumerable, configurable, writable })
}

In this case, the DecoratingIsFun method is run with this set to the object prototype, and it has the opportunity to modify/return a methodDescription, or use the previously specified methodDescription.

Consider DecoratingMakesSense, which makes the doWork method non-writeable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const DecoratingMakesSense = (object, methodName, description) => {
  console.log('Decorating makes sense')

  description.writable = false
  return description
}

class ExampleWithDetailedDecoration {
  @DecoratingMakesSense
  doWork() {
    console.log('can\'t you see I\'m working here?')
  }
}

const makesSense = new ExampleWithDetailedDecoration()
makesSense.doWork()
makesSense.doWork = () => console.log('some other function')
1
2
3
4
5
6
7
8
9
10
11
> node build/main.js
Decorating makes sense
can't you see I'm working here?

/decorator-example/build/main.js:164
makesSense.doWork = function () {
                  ^

TypeError: Cannot assign to read only property 'doWork' of object '#<ExampleWithDetailedDecoration>'
    at Object.defineProperty.value (/decorator-example/build/main.js:164:19)
...

Getting started

Since decorators are currently in the proposal stage, getting started requires a little tweaking of your standard babel/webpack/linter configs.

Babel

1
2
> npm install -g webpack
> npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-plugin-transform-decorators-legacy

In your .babelrc

1
2
3
4
5
6
7
8
{
  "presets": [
    "es2015"
  ],
  "plugins": [
    "transform-decorators-legacy"
  ]
}

Webpack

Here is a barebones webpack.config.js example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var path = require('path')

module.exports = {
  entry: './index.js',
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'main.js'
  },
  module: {
      loaders: [
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: 'babel-loader',
      query: {
        cacheDirectory: true,
        plugins: [
          'transform-decorators-legacy',
        ],
        presets: ['es2015'],
      },
    }
  ]
  },
  stats: {
    colors: true
  }
}

(The important bit is to add ‘transform-decorators-legacy’ to the plugins array.)

Linting error — or is it? 🤔

If you use VSCode, you will probably run across a linter error that says

1
2
3
[js] Experimental support for decorators is a feature that is subject to
change in a future release. Set the 'experimentalDecorators' option to
remove this warning.

This is an error in the VSCode JS support, rather than a linter error.

Add a jsconfig.json file to your project root with the following contents:

1
2
3
4
5
{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

Be sure to restart VSCode, and the problem should go away. If it doesn’t, follow this thread for further information.

Build and run

1
2
3
4
5
6
7
8
9
10
11
12
> webpack
Hash: 63cf378bd6d165758ed8
Version: webpack 3.8.1
Time: 457ms
  Asset     Size  Chunks             Chunk Names
main.js  4.47 kB       0  [emitted]  main
   [0] ./index.js 1.7 kB {0} [built]
   [1] ./decorator.js 195 bytes {0} [built]

> node build/main.js
Decorating is fun
can't you see I'm working here?

For a detailed example checkout this repo. For more examples and a deep dive, checkout wycats’s decorator spec