Understanding TypeScript decorators.

siddharthshyniben

Siddharth

Posted on September 29, 2021

Understanding TypeScript decorators.

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 {

}
Enter fullscreen mode Exit fullscreen mode

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 {

}
Enter fullscreen mode Exit fullscreen mode

Now, Gun will have a ammo property by default.

const gun = new Gun()
console.log(gun.ammo) // => 10
Enter fullscreen mode Exit fullscreen mode

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... 🔫')
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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... 🔫')
    }
}
Enter fullscreen mode Exit fullscreen mode

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})
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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... 🔫')
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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... 🔫')
    }
}
Enter fullscreen mode Exit fullscreen mode

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... 🔫')
    }
}
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
siddharthshyniben
Siddharth

Posted on September 29, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related