An Introduction to JavaScript Decorators
If you ve heard of the term decorators , or spotted them in code snippets, but still aren t sure what they do - then this post is for you. In this post I ll attempt to demystify what decorators are as well as how they can be useful.
Property Descriptors
Before diving into decorators, we must first understand what "property descriptors" are and how they work. In a nutshell - a property descriptor is an object which contains metadata regarding an object property. To view a property descriptor, you can use the Object.getOwnPropertyDescriptor
method. For example:
var foo = {
bar: 'Hello world',
};
Object.getOwnPropertyDescriptor(foo, 'bar');
which will return an object that looks like this:
{
configurable: true,
enumerable: true,
value: "Hello world",
writable: true,
}
Now let s break down what each of these properties represent:
configurable
: indicates whether or not the property descriptor properties can be modified.enumerable
: indicates whether or not the object property will show up in property loops such asfor in
,Object.keys
, etc.value
: the value assigned to the object property.writable
: indicates whether or not the value can be modified.
If you want to update this object, you can use the Object.defineProperty
method. For example, let s make it so that users can t update foo.bar
:
var foo = {
bar: 'Hello world',
};
Object.defineProperty(foo, 'bar', {
writable: false,
});
foo.bar = 'testing testing 1 2 3';
console.log(foo.bar); // => "Hello world"
With this knowledge in mind, we can now move on to decorators.
Decorators
Decorators are prefixed with an @
symbol and are meant for class properties, methods - or the class itself. They allow you to modify (amongst other things) the property descriptor object. Let s explore a couple of potential use cases for this.
Imagine you have a very simple User
class with a firstName
and lastName
property, along with a getName
method that concatenates and logs both properties:
class User {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
getName() {
console.log(`${ this.firstName } ${ this.lastName }`);
}
}
Theoretically, a user could override your getName
method to be anything they like. For example:
const user = new User('John', 'Doe');
user.getName(); // => "John Doe"
user.getName = function () {
console.log('George Washington');
}
user.getName(); // => "George Washington"
as you can see, George Washington
is returned now, even though the first and last name properties are still set to John
and Doe
. To prevent this from happening, we can define a readonly
decorator to update the property descriptor object for getName
- which will prevent it from being rewritten:
function readonly(elementDescriptor) {
elementDescriptor.descriptor.writable = false;
return elementDescriptor;
}
class User {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@readonly
getName() {
console.log(`${ this.firstName } ${ this.lastName }`);
}
}
const user = new User('John', 'Doe');
user.getName(); // => "John Doe"
user.getName = function () {
console.log('George Washington');
}
user.getName(); // => "John Doe"
The elementDescriptor
parameter you see above is detailed here. The elementDescriptor
object contains a descriptor
property - which is the same property descriptor object we described at the beginning of this blog post:
{
configurable: true,
enumerable: true,
value: "Hello world",
writable: true,
}
So by setting the writable property to false, we can prevent users from arbitrarily overriding the method with a new value.
In addition to modifying the property descriptor object, you can intercept methods entirely. For example, let s say we want to console.log whenever a logIn
method is used. We could replace readonly
with a logger
decorator like so:
function logger(elementDescriptor) {
const originalFunction = elementDescriptor.descriptor.value;
elementDescriptor.descriptor.value = function (username, password) {
originalFunction(username, password);
console.log(`${ elementDescriptor.key } called with ${ username } at ${ new Date() }`);
}
}
class User {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@logger
logIn(username, password) {
// login logic here
}
}
const user = new User('John', 'Doe');
user.logIn('JohnDoe', 'abc123');
// => "logIn called with JohnDoe at Mon Sep 09 2019 13:06:21 GMT-0400 (Eastern Daylight Time)"
For the sake of the brevity I ve only given examples that modify a class method, but remember that decorators can also be used to modify class properties or even the class itself.
With the ability to modify metadata or intercept a property entirely - the possibilities are almost endless.
Additional Notes
Decorators are still a stage 2 feature - so if you re interested in giving decorators a spin, you'll need to npm install
the @babel/plugin-proposal-decorators package.
Also, if you re interested in tracking the progress of this feature through the TC39 process, you can do so here.