Using Decorators in JavaScript
Older Article
This article was published 7 years ago. Some information may be outdated or no longer applicable.
Let’s look at decorators in JavaScript, walk through some examples, and flag a few quirks you’ll hit if you start experimenting with them today.
Decorators owe most of their popularity to Angular 2+ and TypeScript, where they sit at the core of the framework. But what actually are they? And why would we want them in plain JavaScript?
At the time of writing, the decorators proposal sits at Stage 2 in the TC39 process. If things go well, decorators will land in the JavaScript language proper. But the proposal could still change, and some of what’s written here may not hold true later.
Decorators in Angular (TypeScript)
Here’s a simple decorator that’ll look familiar if you’ve written (or even glanced at) Angular code:
//some.component.ts
@Component({
selector: 'app-my-list',
templateUrl: './some.component.html',
})
export class SomeComponent implements OnInit {
// ...
}
The class SomeComponent gets extra functionality bolted on by the @Component({}) decorator. Think of it as wrapping the class with additional behaviour. Same concept as functional compositions or higher-order functions (a pattern React leans on heavily).
Put simply, a decorator is a function that extends the capabilities of the element it’s attached to.
We can already write higher-order functions in JavaScript without any special syntax:
function hello(user) {
console.log(`Hi ${user}`);
}
function punctuate(fn) {
return function (user) {
fn(`${user}! Looking great today ;)`);
};
}
const greeting = punctuate(hello);
greeting('John'); // Hi John! Looking great today ;)
Using decorators in TypeScript is possible via the
--experimentalDecoratorscommand line flag fortscor by adding the following configuration totsconfig.json:{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true } }
The Decorator Pattern
The decorator pattern is an object-oriented programming pattern where individual classes get additional functionality tacked on dynamically, without affecting other instances of the same class. For JavaScript developers, this means we can’t (yet) apply higher-order function principles directly to an ES2015 class.
One limitation to keep in mind: decorators can only be used on classes and class members. Nothing else.
Using Decorators Today in JavaScript
Since decorators are still at the proposal stage, we need Babel to transpile them into something browsers and Node.js can run. The @babel/plugin-proposal-decorators plugin handles this.
Note that at the time of writing Babel supports the old version of the proposal (Stage 2, November 2018). The proposal has moved on since then. Use the plugin with the
legacy: trueoption until Babel catches up.
Create a .babelrc file with this content:
{
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
]
}
I’m using Node.js to run the code, with these npm scripts in package.json:
"scripts": {
"babel": "node_modules/.bin/babel decorator.js --out-file decorator.es5.js",
"start": "node decorator.es5.js"
},
Run npm run babel && npm start from the terminal and you’re off.
Class Member Decorator
Here’s how to attach a decorator to a class member function:
class Greeter {
constructor(name) {
this.name = name;
}
@punctuate('!')
hello() {
return `Hi ${this.name}`;
}
}
function punctuate(value) {
return function (target, key, descriptor) {
descriptor.value = function hello() {
return `Hi ${this.name}${value}. You are awesome ;)`;
};
};
}
const greeting = new Greeter('John');
greeting.hello(); // Hi John!. You are awesome ;)
The decorator is just a function (punctuate()), and we can pass parameters to it (though parameterless decorators work fine too). Here we’re overwriting what hello() does. Try swapping the ! for a ? and see what changes.
Let’s crack open the decorator function and inspect its parameters:
function punctuate(value) {
return function (target, key, descriptor) {
console.log('target', target);
console.log('key', key);
console.log('descriptor', descriptor);
};
}
// returns ==>
target Greeter {}
key hello
descriptor { value: [Function: hello],
writable: true,
enumerable: false,
configurable: true }
The target is the class itself. The key is the class member function (confirming that decorators operate on specific class methods). The descriptor is the object describing the data or accessor. You’ve probably seen descriptor objects before with Object.defineProperty():
Object.defineProperty({}, 'key', {
value: 'some value',
configurable: false,
enumerable: false,
writeable: false,
});
Since we’ve got access to these property values, we can flip writeable from true to false. One decorator, and the class member becomes read-only:
class Greeter {
constructor(name) {
this.name = name;
}
@readonly()
hello() {
return `Hi ${this.name}`;
}
}
function readonly() {
return function (target, key, descriptor) {
descriptor.writable = false;
return descriptor;
};
}
const greeting = new Greeter('John');
greeting.hello = function () {
return 'Never gets called! :(';
};
console.log(greeting.hello()); // Hi John
The new hello function never fires. Remove the
@readonlydecorator and watch the behaviour change.
We can also use this technique on class members that aren’t methods.
For the example below, you’ll need
@babel/plugin-proposal-class-propertiesadded to.babelrcwithloose: true. Class properties aren’t part of the JavaScript language yet.
class Greeter {
@readonly name = 'John';
hello() {
return `Hi ${this.name}`;
}
}
function readonly(target, key, descriptor) {
descriptor.writable = false;
return descriptor;
}
const greeting = new Greeter();
greeting.name = 'Jack';
greeting.hello(); // Hi John
The name property can’t be overwritten because the decorator has locked it down.
For comparison, here’s how the
targetobject looks with thelegacy: falseoption in.babelrc:
function punctuate(value) {
return function (target) {
console.log(target);
};
}
// returns ==>
Object [Descriptor] {
kind: 'method',
key: 'hello',
placement: 'prototype',
descriptor:
{ value: [Function: hello],
writable: true,
configurable: true,
enumerable: false } }
Same information, but this time it comes back as a complete descriptor object.
Class Decorators
So far we’ve decorated class methods. But you can also decorate an entire class. The key difference: a class member decorator only applies to the method or property right below it, while a class decorator applies to the whole class. Class decorators can also accept parameters.
Here’s an example:
@isEmployee
class Greeter {
constructor(name) {
this.name = name;
}
hello() {
return `Hi ${this.name}`;
}
}
function isEmployee(target) {
return class extends target {
constructor(...args) {
super(...args);
this.isEmployee = true;
}
};
}
const greeting = new Greeter('John');
greeting; // Greeter { name: 'John', isEmployee: true }
We’ve bolted a new property onto the class using the decorator annotation.
How does this look with legacy: false? It takes more code, but the result is identical:
@isEmployee(false)
class Greeter {
name = 'John';
hello() {
return `Hi ${this.name}`;
}
}
function isEmployee(value) {
return function (descriptor) {
const { kind, elements } = descriptor;
const newElements = elements.concat([
{
kind: 'field',
placement: 'own',
key: 'isEmployee',
initializer: () => {
return value;
},
descriptor: {
configurable: true,
writable: false,
enumerable: true,
},
},
]);
return {
kind,
elements: newElements,
};
};
}
const greet = new Greeter();
greet; // Greeter { name: 'John', isEmployee: false }
Without passing a parameter to the decorator, it’d look like this:
function isEmployee(descriptor) {
const { kind, elements } = descriptor;
const newElements = elements.concat([
{
kind: 'field',
placement: 'own',
key: 'isEmployee',
initializer: () => {
return false;
},
descriptor: {
configurable: true,
writable: false,
enumerable: true,
},
},
]);
return {
kind,
elements: newElements,
};
}
TC39 Specification and core-decorators.js
I’d recommend reading the proposal and spec for decorators. It covers built-in decorators (ones that’ll ship with JavaScript) and much more. Also worth checking out: core-decorators.js, which gives you access to some stage-0 decorators defined in the TC39 spec.
Conclusion
Decorators are genuinely useful. We can already use them in TypeScript (with the right flags flipped), and JavaScript support is coming. I’ve tried to flag the quirks I ran into while experimenting. The spec is still shifting, but once decorators land in the JavaScript standard, they’ll give developers a clean way to extend class behaviour.