JavaScript Shinies: Decorators
In an ongoing series about JS features that are either newly here or coming soon we look @ decorators, a new feature to allow us to wrap functionality around our functionality.
Ok, we’re getting a little deeper now. And this is a long article, so my apologies in advance.
Conceptually, a decorator is a function that wraps access to class members, whether they be properties or functions. It should be reiterated that these have to be class properties, this functionality doesn’t work on raw functions or other objects. Well, it can, but it’s not intuitive and not what we want to talk about here.
In some ways this is not hugely dissimilar to the proxies we looked at previously, at least conceptually. They’re both specific functions to handle access to object properties.
What Problem Does It Solve?
Let’s take a semi-real case.
class Transaction {
constructor(transactionData = {}) {
Object.assign(this, transactionData);
}
setName(name) {
this.name = name;
} setTransactionTime(transactionTime) {
this.transactionTime = transactionTime;
}}
There’s nothing particularly fancy here. This is a DTO (Data Transport Object) that has whatever properties the object passed into the constructor has. You then have some other methods to set the transaction time and name. Arbitrary choices of properties that may need “fixing” or whatever.
Using this is going to be pretty simple.
const transaction = new Transaction(data);
transaction.setTransactionTime(new Date);
transaction.setName('Matt');
So we want to make a pretty simple and not all that useful decorator first just to look at the syntax. We want to dump out a console log, to say when we’ve triggered one of our “watched” functions. We’re not going to do anything fancy with it, we just want to tag it and have it tell us that it was used.
To do this we need to make a simple function, which has to follow this signature.
const myDecoratorFunction = (target, name, descriptor) => {
// whatever the decorator needs to do
}
The code we need is something simple. This function has three arguments. The first, target
should obviously refer to the function you called. I say that specifically because it would be obvious if it did, but it doesn’t. The target
in this case is pretty much always the class. Not the function. The name
argument is a string of the function name called (in this case we’ll use watched
).
The last one ,and probably the most important, is the descriptor. This is ironically hard to describe, but is basically an object that describes the property. It’s worth a deeper look at this.
Descriptors
Every property of a class, method or otherwise, has something called a descriptor. The vast majority of the time in normal coding, this is not really used for much.
const accessRecord = {};accessRecord.firstEditor = "Alex Smith";Object.defineProperty(accessRecord, "lastEditor", {writable: true});accessRecord.lastEditor = "Sam Jones"; // no problem
You can see from this that the first property we just dynamically create as we assign it and the vast majority of the time that’s totally fine. The second one we create with defineProperty
and the third argument we pass it is a descriptor. Note that we have to explicitly say it’s writeable, otherwise it will not be. We can also set the value at the same time.
Object.defineProperty(accessRecord, 'dateCreated',{
value: new Date,
writable: false
});accessRecord.dateCreated = new Date(2018, 11, 24, 10, 33);
This simply won’t work. It will seem to work, but the property’s value won’t change, and this is by design. Our property is explicitly not writeable.
The descriptor object contains a few properties related to the class property. Let’s have a look at one. You can actually get access to one by doing this.
Object.getOwnPropertyDescriptor(accessRecord, 'dateCreated');
Which will return something like this. This isn’t the actual output from above, by the way, it’s just an example from something I was looking at when I remembered I needed a screenshot.
The first property, configurable
isn’t that interesting (it’s largely whether or not the property can be removed or modified). The enumerable
property is whether the function shows up on a list of properties, such as Object.entries
or for x in object
.
We’ve already looked at it a bit, but writable
is far more interesting. This just determines whether or not the value can be changed. In JavaScript’s wild and wooly world the following is completely valid.
const myTransaction = new Transaction(testData);
myTransaction.setName = 'NOT-A-FUNCTION-NOW-LOL';
We don’t necessarily want people to be able to do that. The solution is simple, and is almost the definitive example of a decorator.
const readOnly = (target, name, descriptor) => {
descriptor.writable = false;
}
This isn’t a decorator, it’s just a function. But it can be used as a decorator by so prepending @readOnly
to our setName
function. This modifies the descriptor, so it’s no longer editable.
The last and most interesting property here is value
. In the cases we’ve talked about, the value
is simply the function definition.
So here’s the code we’d need to make a decorator that console logs out our function being accessed.
const watched = (target, name, descriptor) => {
console.log(`Function ${name} called.`);
}
We can make any function in our class here trigger this logging behaviour by appending it with @watched
like so.
@watched
setName(name) { ... }
When the Transaction.setName
function is called, it will trigger the watched
function first. There will be a console.log
called and then the function will execute and update the name.
Replacing functionality
Decorators don’t just have to add to the functionality, though, they can also intercept it entirely.
Let’s just say we want to log out more debug info from one of these functions. We want to know what function was called, what its arguments were, and what the result was. We could do that pretty easily. Put a console.log
at the top, another at the bottom, etc.
setName(name) {
console.log(`Function setName called with argument: ${name}`);
this.name = name;
console.log('Completed and did not return anything');
}
You’ll see that gets pretty messy quite quickly. And more importantly, none of this debug code actually has anything to do with setName
. The only thing in the setName
function should be… setting the name.
So we can do it as a decorator and get rid of all that irrelevant code. Not only that, but we can reuse it all over the place on other functions and have it work nicely there as well.
We’ll start off by making a function to use as the decorator.
const debug = (target, name, descriptor) => {
console.log(`Calling ${name} with arguments: ${args}`);
// call the original method and create result
console.log(`result is %o`, result);
}
This will do something very close to what we want, except the result
isn’t set so it can’t display that. To do so we need to actually call the original function (which is located on target.value
, remember) and use its return value. There are a couple of assumptions that are obvious but mistaken in subtle ways.
For every complex problem there is an answer that is clear, simple, and wrong. — H. L. Mencken (though misquoted)
let result = descriptor.value();
This seems obvious, but it won’t support any arguments. It also has a few other subtler issues we’ll deal with soon. You actually need to assign a new function to the descriptor, and then have that call a saved version of the original function. It’s easier to show it right once than repeatedly do it wrong and explain why it’s wrong.
const debug = (target, name, descriptor) => {
const method = descriptor.value; descriptor.value = function(...args){
console.log(`Calling ${name} with arguments: %o`, args);
let result = method.apply(this, args);
console.log(`result is %o`, result);
};
}
There are a couple of things here that aren’t obvious. First we make a method
constant that is just the original function. It may or may not be clear that method
isn’t anything official. You could call it oldFunction
or daFunk
all you like. We then assign the descriptor value as a new function. A trap for young players (me specifically) is that this seems obvious:
descriptor.value = (...args) => {}
This will not work. Or more particularly, doing it that way will rebind the value of this
to the current scope, and so our call to this.name
will fail because this
is undefined. It is necessary to explicitly use function(...args)
.
We can then do our console log in that function, and we can use method.apply()
to trigger a function call.
We haven’t talked about apply previously, it’s not exactly new, but what it does is lets you run a function, explicitly passing into it a value for this
and an array of arguments.
The value of this
in this case isn’t super intuitive. At the time the function is defined here, in our decorator, there is no meaningful value for it. It’s part of a global function. But at the time the decorator function is called, this
has the value of the class, and that’s what’s available in this function. This is why our fat arrow function doesn’t work here. It’s correctly “reassigning” the value of this
to the new scope, but we actually want the old value. This is the exact behaviour we’re trying to avoid by using fat arrows most of the time.
I should note it’s possible that the paragraph or two above is riddled with lies. I have an only tentative understanding of how that part works.
So our code above replaces the existing function within the descriptor with another function, wrapping it up in some debug. This process, replacing one function call with another, is known as Call Forwarding.
As an aside, using apply()
isn’t the only way. You can use the call()
function which does the same thing except it expects the arguments to be passed in order, rather than an array. We can also use our rest syntax to get those arguments. These two are identical. Apply in this case is slightly shorter but more particularly seemed a better fit: our arguments were already an array.
let result = method.call(this, ...args);
let result = method.apply(this, args);
Another example — Making a function fluent
Our functions above work. We can do something like this example as shown above.
const transaction = new Transaction(data);
transaction.setTransactionTime(new Date);
transaction.setName('Matt');
Still, it all seems very wordy. It would be nicer to make a fluent interface to this.
transaction.setTransactionTime(new Date).setName('Matt');
A fluent interface is just an interface where the methods all return this
, so that you can chain them.
That’s not hard, we just need to return this
from the set methods. We could just literally do that, but where’s the fun in that? Instead we can make a decorator function to do so.
There’s arguably no real benefit here. You need to add a line either way. It’s reasonable to suggest that the decorator is still a nicer solution. It explicitly communicates that the method can be used fluently, while simultaneously meaning that implementation details of that interface don’t need to be visible in the code. The setter can just concentrate on setting.
function fluent (target, name, descriptor) {
const method = descriptor.value;
descriptor.value = function(...args){
method.apply(this, args);
return this;
}
}
No new ground here, so we’ll skip by the line by line. Again, we’re using call forwarding, and just returning the context already passed in by the decorator’s calling scope. The reason to show this demo is to give a nice example of a simple decorator that can add subtle improvements to how you structure code.
Decorator Arguments
All of the above is relatively easy and comprehensible, so let’s kick it up 143 notches by allowing an argument.
@debug('Data Access')
getProduct(id) {
// irrelevant implementation details
}@debug('Local State')
setCurrentProduct(id) {
this.id = id; // also irrelevant but short
}
You can see in the above example that we would expect completely different looking debug, something to mark what sort of debug this is doing.
The way this now has to work is substantially more complicated that than you might think. Specifically that rather than a function which is a decorator, you now need a function (which is the arguments) which returns a function which is a decorator.
To expand our debugging decorator above is actually quite simple.
const debug = debugName => {
return function(target, name, descriptor){
const method = descriptor.value;
descriptor.value = function(...args){
console.group(debugName);
console.log(`Calling ${name} with arguments: %o`, args);
let result = method.apply(this, args);
console.log(`result is %o`, result);
console.groupEnd();
return result;
};
}
}
A couple of things of note here. The first is that even though the decorator function has to be a function(){}
because of the scoping of this
, the outer arguments function can indeed be a fat arrow function. There is probably no requirement for it to support this
.
We’re using console.group
as documented officially here or in an article by me here. It basically just lets you label and collect a bunch of logs.
Nothing elaborate, but occasionally useful nonetheless. You can actually nest them, too, which can better reveal structure in output, but isn’t really relevant here.
Decorator Arguments — Multiple Arguments
Going from this to multiple arguments is trivial. Our debugName
isn’t a useful example, but we could at least debug out the arguments used. Again, rest/spread syntax is our friend.
const debug = (...debugArgs) => {
return function(target, name, descriptor){
const method = descriptor.value;
descriptor.value = function(...args){
console.log(debugArgs);
console.log(`Calling ${name} with arguments: %o`, args);
let result = method.apply(this, args);
console.log(`result is %o`, result);
return result;
};
}
}
Decorators On Class Properties
We’ve got this running on functions, but actually there’s no reason it could only trigger on function calls.
Since we’ve looked at class properties previously they’re also a great candidate for decorators. You can happily do something like this.
class Article { @readOnly @timestamp creationTime = new Date;}
Note that whitespacing. When putting decorators on a class property this is a standard syntax, but class and function decorators near universally get one line per decorator.
Making a timestamp
That @timestamp
example is a good one, so let’s use it. Let’s say we have a @timestamp transactionTime
. That might come from something like MySQL or (in my case) the Ethereum Virtual Machine, and be set as an integer timestamp. And just for fun it’s a Unix timestamp — seconds, rather than JavaScript’s default milliseconds.
We end up with ambiguity. This value can be milliseconds, seconds, or a date, seemingly at will. (If you’re wondering why it was sometimes milliseconds… so am I, but it happened.)
What we want to do, then, is make it so that the value is always a proper JavaScript date date. Not a string or an integer — a date. One of the major definitions of an Object Oriented Language is passing objects around. Not strings or integers… objects. That’s its greatest strengths, and in fact not doing so is a named anti-pattern called Primitive Obsession.
We can do this a few ways by defining a setter on the property using a decorator. The syntax is simpler than you might think. A setter is just one of the properties of the property descriptor, which we looked at earlier. We didn’t specifically note this one.
Anyway, it’s basically just this.
const isTimestamp = (target, name, descriptor) => {
descriptor.set = value => {
if (value instanceof Date) {
return value;
}
if (!value.isNum()) {
throw Error('This is not a supported date format');
}
value = Number(value);
if (value.length < 9) {
value = value * 1000;
}
return new Date(value);
}
}
This is a tiny bit long because we’re covering a lot of different states the setter could try and handle. The first one is the simplest. If it’s already a date, just set it. The next is just to back out of the whole thing if someone tries to save some strange string.
If not, we know we have an integer, or at the very least a string of an integer, so we deal with that next. Number(value)
just converts it to an integer, though there are other ways of doings so. My actual preference is just to do value = +value
and let the unary operator work. Another common solution is parseInt
, which I do not recommend. It will happily and rightly convert "123"
to 123
but it will also happily and wrongly convert "12 Monkeys"
to 12
.
If our value is a too-short integer then it’s a Unix timestamp and should be multiplied to get milliseconds, and whatever we have now is suitable to use as a Date constructor value. Note of course that this wasn’t intended to catch every possible use-case. You could pass a value of 4, for example, and it would be perfectly valid, but probably not create a useful date object. We’re just covering reasonable expected values.
class Transaction { @isTimestamp transactionTime;
constructor(userData) {
Object.apply(this, userData);
}}
Again we could use an argument to provide a specific format and make the above code easier: @isTimestamp('unix')
.
Class Decorators
There’s another type of decorator that got only a brief mention, which is that the entire class can have a decorator.
@whatever
class MyClass {
useful = false;
}
In this terrible example, the whatever
function will execute on the constructor of a MyClass
instance.
The whatever
decorator isn’t much to look at.
const whatever = constructor => {
// do something
}
The use of the word constructor
there is confusing. Another way to put it would be target
like we did before for our function constructors, or Class
making sure to avoid a lowercase version because that’s a reserved word. The reason constructor
is used in the documentation is because that’s what it is.
When it says “constructor” that’s not the same as the class constructor function. Everything in JavaScript is a function under the hood, and that includes our class. It essentially has a “core” function, whose “prototype” is then extended with additional functionality. If you log out the constructor returned by this decorator you get this.
In the end it really is all just functions.
Now that we’re fiddling with the constructor we can do a number of things. For example, we could just return an object of our choosing. Or we could actually create a new one of whatever’s being requested. It might seem obvious to do something like this, but it won’t work.
const whatever = constructor => {
return constructor(); // like this?
return new constructor(); // or this?
}
These options won’t work. A constructor returns an object, and the decorator expects a function.
const whatever = constructor => {
return () => new constructor();
}
Amazing. In just three lines of code we’ve achieved… well… nothing at all. But it’s not hard to see why you might mess with the constructor’s arguments or do a few other things.
const hasBigButt = constructor => {
return () => {
const newClass = new constructor();
newClass.bigButt = true;
newClass.canLie = false;
newClass.anacondaWantsSome = true;
}
}
Let’s use the class decorator to do something more genuinely useful.
I have a class that I use to handle abstracting Ethereum blockchain contract javascript artefacts that are used for data access. This is its basic skeleton.
export default class ContractWrapper {
contract = null;
extended = []; constructor(contract, web3 = false) {
this.contract = contract;
if (web3) {
this.web3 = web3;
}
} loadDependencies = dep => {
return this.extended.push(dep);
}
}
The usage of it is relatively simple.
import Web3 from 'web3';import MyToken from 'build/contracts/MyToken.json';
import ERC20 from 'build/contracts/ERC20.json';const contract = new ContractWrapper(MyToken, Web3);
contract.loadDependencies(ERC20);
The problem is the API isn’t great. The loadDependencies
step is easy to skip and could be made more intuitive. The problem is that the web3
argument is last. Which means the contract arguments can’t be variadic. Or the web3
argument couldn’t be optional, and passing it as the first argument with false
would be kind of weird. The point of having it optional is that if not passed in it can be set up with some sensible defaults.
So what we’re going to do is make the ContractWrapper
class @extendable
with a decorator. This will let us do something like this.
const contract = new ContractWrapper(MyToken, BaseToken, ERC20);
I’m going to put in the working decorator and then go through it.
const extendable = target => {
return (...contracts) => {
const last = contracts[contracts.length-1].constructor.name; const web3 = last === 'Web3' ?
contracts.pop() :
false; let newClass = new target(contracts.shift(), web3);
contracts.forEach(dep => newClass.loadDependencies(dep));
return newClass;
}
}
Most of this isn’t wildly different from what we’ve covered previously. It’s just doing a bit more fiddling. We start off by checking to see if the last thing passed in is a web3 instance, and if it is, pop it off and store it. We then use that and the first argument passed in as the constructor arguments. If we have other arguments we load them as dependencies. (If we don’t have any left it’s an empty array and doesn’t do anything, so we don’t bother checking for that.)
Now we can use the interface above and it will work perfectly.
It should, of course, be noted that messing with the api of your classes like this isn’t always a great idea. Someone coming to this class and looking at its constructor would have little or no idea what this weird extendable stuff does and would rightly assume passing in a single artefact and an optional web3 instance. You could literally make a decorator that swaps around every argument or replaces them with the names of ska bands. Doing so would just make you a psychopath. Or at least, that’s the impression that I get.
In truth, class decorators are vastly less common than property and function decorators, except in Angular, which we’ll talk about later. It’s actually quite hard to come up with good use cases, but some involve things like caching, creation of objects as singletons, etc.
A Critical Point About The Mental Model
It important to clarify that the point of a decorator is not to change how a function or property executes. It’s to change how a function or property is defined. These shouldn’t be thought of as a sort of runtime execution, they should be thought of as a way to change how the class is defined and structured.
Fundamentally this doesn’t really change things, but is a useful part of the mental mapping that might help you get the hang of these doodads.
This is why the above actually works. It might seem that you’d be getting infinite loops by running doing a new item in the decorator, as that would then trigger the same decorator. But it doesn’t, because it’s triggered on the definition of the class, not its invocation.
Usage In Frameworks
Like a lot of other functionality in JavaScript there’s a lot of this stuff in advanced usage in modern frameworks. In React, decorators are occasionally used by some people for handling things like Redux connect boilerplate. In fact there’s a competing state management tool called MobX that is almost entirely built around decorators.
Ember is adding first-class support for decorators for things like @computed
, @action
, and @service
, in line with fully-fledged move towards native JS features over custom syntax.
But by far the most extensive use of decorators is in Angular, where they’re a key part of how components are defined, much like React class components extend an imported component
class.
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
Of course, this is typically done in TypeScript rather than JavaScript, but it stands. Angular uses these extensively, allowing it to hive off large sections of boilerplate into these out-of-sight decorators that are then passed the necessary data objects.
How To Enable This
It’s not too hard to make it work. It’s impossible for me to know what build tools you’re using, but the process should be roughly the same.
This guide will be largely the same as for class properties, and the process will be near identical, starting with an NPM install.
npm install --save-dev @babel/plugin-proposal-decorators
Then you need to add to the plugins entry in the .babelrc
file, assuming you have one. If not, you need to make one with something like this syntax.
{
"plugins": [
["@babel/plugin-proposal-decorators"]
]
}
Things get a bit more complex when we have the class properties enabled, which is recommended — they work well together.
{
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
["@babel/plugin-proposal-class-properties", { "loose" : true }]
]
}
It’s critical that the decorators come first, and the combination of the argument objects are also necessary.