I’ve been looking at the new features in ES6 and boy has JavaScript changed a lot in the last few years. The ECMAScript standards team is really doing a great job making the language more comfortable to work with. My favorite features are block scoped variables with let
, a better syntax for defining a class
(I never really understood what a “prototype” was), built-in support for loading and exporting modules, and native support for promises. Proxy objects seem like they could be a powerful tool as well. My first impression is that JavaScript is starting to look like a more liberal version of Python, which isn’t a bad thing.
One of the features I looked for in ES6 and could not find was support for function decorators. Decorators can be a great thing to have in a language sometimes. When they fit into an api, they really fit in well and I often use them in my Python library code. I was surprised this feature didn’t make it into ES6 because they are used extensively as part of Angular and React and have native support in TypeScript.
The proposal for decorators can be found in this repository with user guide located here. The proposal is currently at stage two which means it will likely be included in the language in the next major update but are not ready to be included in production code yet. I wrote some sample decorators to test out the new features which you can find in my notes repository here and I’d like to explain to you how they work.
Note: I am not an expert JavaScript, front-end, or Node developer so if you see anything I’m doing wrong in these examples, please let me know in the comments or in an email.
Building the Project
Unfortunately, there is no native implementation for decorators in Node (currently version 11) or any browser yet so you must compile your decorator code with a project called Babel. This project requires two additional plugins, @babel/plugin-propsal-decorators
and @babel/plugin-proposal-class-properties
.
npm install --save-dev @babel/cli \
@babel/core \
@babel/plugin-proposal-class-properties \
@babel/plugin-proposal-decorators
Babel must also be configured with some options in a .babelrc
file you can find here.
Now you can compile and run your code with babel like this and it should work correctly:
babel ./index.js -o index-compiled.js && node ./index-compiled.js
Anatomy of a Decorator
A decorator is basically just a function that gets called in the context of a target method or property that is able to change its state somehow. Here is an annotated example of a decorator which does nothing:
function(descriptor) { // alter the descriptor to change its properties and return it descriptor.finisher = function(klass) { // now you get the class it was defined on at the end } return descriptor; } class Example { @decorator decorated() { return 'foo'; } }
The descriptor
that is returned from the decorator is an object that you can mutate to change the target method. The important properties of this object are:
kind
– whether this is a ‘method’, a ‘field’, or something elsekey
– the name of what is being decorated (in this case, ‘decorated’)descriptor
– contains configuration for the property, and thevalue
which you can hook into with custom behaviorfinisher
– add a function to be called after the class is defined for customization of the class
To have your decorator take parameters, use a function that returns a decorator like the one above.
function decorator(options) { // options are passed with the decorator method return function(descriptor) { return descriptor; } } class Example { @decorator({foo:'bar'}) decorated() { return 'foo'; } }
Example: Log a Warning When Calling a Deprecated Method
function deprecated(descriptor) { // save the given function itself and replace it with a wrapped version that logs the warning let fn = descriptor.descriptor.value; descriptor.descriptor.value = function() { console.log('this function is deprecated!'); return fn.apply(this, arguments); } return descriptor; } class Example { @deprecated oldFunc(val) { return 'oldFunc: ' + val; } } let ex = new Example(); ex.oldFun('foo') // > this function is deprecated! // > oldFunc: foo
Example: Make a Property Readonly
function readonly(descriptor) { descriptor.descriptor.writable = false; return descriptor; } class Example { @readonly x = 4; } let ex = new Example(); ex.x = 8; ex.x; // returns 4. note that you don't get a warning for trying to set a readonly property
Example: Reflect a Class
Given a class, we want to find all the methods that are decorated with a certain decorator. We will use the @property
decorator for this. This sort of thing would normally be used for a base class in your API that your user is expected to override.
function property(descriptor) { descriptor.finisher = function(klass) { klass.properties = klass.properties || []; klass.properties.push(descriptor.key); }; return descriptor; } class Example { @property someProp = 5; @property anotherProp = 'foo'; static listProperties() { return this.properties || []; } } Example.listProperties() // > [ 'someProp', 'anotherProp']
Conclusion
Decorators open up a lot of possibilities in a language and I am looking forward to their inclusion into JavaScript. I plan to use them in a Node library I am writing right now. However, reflection is a very powerful tool and should not be used without careful consideration. Make sure the decorator pattern actually fits your use case before you decide to use them. Have fun with decorators!
Thanks for the write-up!
I recently also started fiddling around with the new proposal’s babel implementation.
Was trying to find a way to implement “soft”-privacy for fields using decorators.
I’m not claiming it should be done using decorators but I was curious if we could achieve something like that.
https://codesandbox.io/s/xv68r7z48p
With the old implementation that almost worked but due to the “loose” param for the
plugin-proposal-class-properties that didn’t work either.
Wondering if there is a way I don’t see yet 🙂