Decorators, a.k.a annotations, are a new feature coming in ES7. A decorator is basically an expression that evaluates to a function, takes the target object as a parameter and returns the modified object. Decorators are very similar to Annotations in Java, but unlike Java annotations, decorators are applied at runtime.
If you are reading this and your background is Python or Java, you may begin to imagine what decorators are good at but for the rest of you here are some nice examples:
Class decorators
// We begin by declaring the decorator. function SimpleDecorator(targetClass){ // We define a new static property targetClass.decorated = true; // And a new instance property; targetClass.prototype.alsoDecorated = true; return targetClass; } // Applying the decorator is very easy @SimpleDecorator class SimpleClass { } const instance = new SimpleClass(); console.log(SimpleClass.decorated); // -> true console.log(instance.alsoDecorated); // -> true
Pretty easy right? A class decorator allows as to “decorate” a class with both static and instance properties or methods.The decorator takes the class in question as a parameter and returns it after it makes all the necesary modifications to it.
A decorator can also take additional parameters.
// If we want to set additional parameters, our decorator must return a function. function SetClassId(classId){ return function(targetClass){ targetClass.prototype.classId = classId; return targetClass; }; } // Pass a random value from 0 to 999 as a class id @SetClassId(parseInt(Math.random()*999)) class SimpleClass { } // As you can see all instances got the same value. // That's because the decorator is executed once on class definition. console.log(new SimpleClass().classId); // -> 754 console.log(new SimpleClass().classId); // -> 754 console.log(new SimpleClass().classId); // -> 754
Let’s try to implement a Singleton decorator. The decorator will add a static getInstance() method that will return the same class instance every time it’s called.
// Decorator definition function Singleton(targetClass) { // We will hold the instance reference in a Symbol. // A Symbol is a unique, immutable property of an object introduced in ES6. const instance = Symbol('__instance__'); // We define the static method for retrieving the instance. targetClass.getInstance = function () { // If no instance has been created yet, we create one if (!targetClass[instance]) { targetClass[instance] = new targetClass(); } // Return the saved instance. return targetClass[instance]; }; return targetClass; } @Singleton class Counter { constructor() { this._count = 0; } increment() { this._count++; } get count() { return this._count; } } // Get two different references of the Counter instance. const a = Counter.getInstance(); const b = Counter.getInstance(); console.log(a.count); // -> 0 a.increment(); // because a and b point to the same instance of Counter, b.count is also incremented. console.log(b.count); // -> 1
Method decorators
Decorators will work on class methods also. If the decorator function took just one parameter when decorating a class, when decorating a method the parameter list changes a bit. The function will take three parameters when decorating a method.
- Class instance reference.
- Method name.
- Object property descriptor.
Formatting data using method decorators:
// Method decorators take three parameters function Format(target, name, descriptor){ // Keep a reference to the original getter const getter = descriptor.get; descriptor.get = function(){ // Call the original getter and return the modified result. return getter.call(this).toUpperCase(); }; return descriptor; } class Person { constructor(name){ this._name = name; } @Format get name(){ return this._name; } } const dude = new Person('bob'); console.log(dude.name); // -> BOB
Validating data using method decorators:
// Method decorators take three parameters function Format(target, name, descriptor){ // Keep a reference to the original getter const getter = descriptor.get; descriptor.get = function(){ // Call the original getter and return the modified result. return getter.call(this).toUpperCase(); }; return descriptor; } function Validate(target, name, descriptor) { // Keep a reference to the original setter const setter = descriptor.set; descriptor.set = function (value) { // If the provided value is invalid throw an error if (value < 0) { console.log('Value must be positive'); } else { setter.call(this, value); } }; return descriptor; } class Person { constructor(name) { this._name = name; } @Format get name() { return this._name; } @Validate set age(value) { this._age = value; } } const dude = new Person('bob'); console.log(dude.name); // -> BOB dude.age = -10; // -> Value must be positive
Closing thoughts
Although no browser supports these features yet, you can still use them in your production code. For transpiling ES6 code into ES5, I recommend Babel. Combine this with Webpack and you’ll get yourself a state of the art building and developing environment.
P.S: In order to transpile decorators with Babel, you’ll need to use stage-1 preset.
P.P.S: Babel 6 doesn’t support decorators yet. You have 2 workarounds:
- Use Babel 5.
- Use Babel 6 with babel-plugin-transform-decorators-legacy plugin.
That’s it. Go build something awesome.