JavaScript is an awesome programming language. And TypeScript has done a great job at filling in the gaps of JavaScript. Not only does it add types, it also implements a few extra features which aren't there in JavaScript yet. One of them are decorators.
What is a decorator?
Decorators have been there in programming languages for a long time. Definitions vary, but in short a decorator is a pattern in JavaScript which is used to wrap something to change it's behavior.
In both JavaScript and TypeScript this is an experimental feature. In JavaScript, it's still a Stage 2 proposal and you can only use it via transpilers like Babel.
I've decided to explain TypeScript decorators because in TypeScript it has been standardized, and both are basically the same anyways.
Using Decorators
This is a very simple example of how to use a decorator:
const myDecorator = (thing: Function) => {
// return something
}
@myDecorator
class Thing {
}
First we define a function myDecorator
, and then we "decorate" a variable (our class Thing
in this case) with the decorator. The decorator can return pretty much anything, but most of the time it used to set properties on the class, etc. Here's a real life sample:
const defaultGun = (gun: Function) => class extends gun {
ammo = 10
}
@defaultGun
class Gun {
}
Now, Gun
will have a ammo
property by default.
const gun = new Gun()
console.log(gun.ammo) // => 10
Decorating functions
Another place we can use decorators is in class methods. This time, the decorator gets three arguments.
const myDecorator = (parent: Function, prop: string, descriptor: PropertyDescriptor) => {
// return something
}
class Gun {
@myDecorator
fire() {
console.log('Firing in 3... 2... 1... π«')
}
}
The first param contains the class where the decorated thing exists (in our case Gun
). The second param is the name of the property decorated (in our case fire
). The last is the property descriptor, which is the output of Object.getOwnPropertyDescriptor(parent[prop])
Properties
You can also decorate properties. It is pretty much the same as function decorators, except there is no third parameter:
const myDecorator = (parent: Function, prop: string) => {
// return something
}
More places to decorate
You can also decorate in more places. Check out the documentation to learn more.
Use cases
There are many uses for decorators. We'll go over some here.
Calculate performance
class Gun {
@time
fire() {
console.log('Firing in 3... 2... 1... π«')
}
}
time
could be a function which calculates the execution time.
Decorator factory
Decorators can also be factories, which returns a function which is the actual decorator. This can be useful when you want your decorators need any arguments.
// The decorator factory
const change = value => {
// The factory will return a new handler
return (target, prop) => {
// We replace the old value with a new one
Object.defineProperty(target, prop, {value})
}
}
Then when "decorating" we just need to decorate like a function:
class Gun {
@change(20)
ammo = 10
}
const gun = new Gun();
console.log(gun.ammo) // => 20
A practical example: error handling
Let's use what we have learned to solve a real world problem.
class Gun {
ammo = 0
fireTwice() {
console.log('Firing in 3... 2... 1... π«')
}
}
To fire twice, we need at least 2 ammo. We can make a check for that using a decorator:
const minAmmo = (ammo: number) => (
target: Object,
prop: string,
descriptor: PropertyDescriptor
) => {
const original = descriptor.value;
descriptor.value = function (...args) {
if (this.ammo >= ammo) original.apply(this);
else console.log('Not enough ammo!');
}
return descriptor;
}
minAmmo
is a factory decorator which takes a parameter ammo
which is the minimum ammo needed.
We can use implement it in our Gun
class.
class Gun {
ammo = 0
@minAmmo(2)
fireTwice() {
console.log('Firing in 3... 2... 1... π«')
}
}
Now if you run fireTwice()
, it won't fire because we don't have enough ammo.
The nice thing is that we can just reuse this without rewriting an if statement. Suppose we need a fireOnce
method. We can easily implement that.
class Gun {
ammo = 0
@minAmmo(2)
fireTwice() {
console.log('Firing twice in 3... 2... 1... π«')
}
@minAmmo(1)
fireOnce() {
console.log('Firing once in 3... 2... 1... π«')
}
}
This kind of decorator can be really useful authentication. authorization, and all the other good stuff.
Liked the post? β€οΈ it. Loved it? π¦ it.
If you want more people to learn about this, share this on Twitter
Top comments (4)
Mhm β¦ is it just me or are some examples really inconsistent/unverified?
As in creating "gun", but logging the ammo of "car"?
Or stating to call "fire" with minimum ammo decorator although the function changed its name to fireTwice?
Whoops, my bad. Fixed them all. Thanks for pointing out!
All the likes and saves of this article. People that don't read the article.
i honestly didn't understand this i got lost in the midway. Especially on the
const gun = new Gun()
consol.log(gun.ammo)