Algobook
- The developer's handbook
mode-switch
back-button
Buy Me A Coffee
Fri Mar 24 2023

Decorators in TypeScript

In this post we will take a look at Decorators in TypeScript and do some examples. But first, what is decorators? Well, it is basically a way of changing the behaviour of the property that we are wrapping it with. It can be a class, a function or a property of a class.

Decorators in TypeScript are marked as experimental, however, they have been around for quite some time and are stable enough to be used in production. To read more in depth about decorators, have a look at the TS docs.

Now, lets begin with some practical examples.

Prerequisites

  • TypeScript project set up

Apply this to tsconfig.json:

{ "compilerOptions": { "experimentalDecorators": true } }

Let start with a class decorator

In our first example, we will create and use a class decorator.

Let's define our class first:

@ElectricVehicle @Logging export class Tesla { batteryPercentage: number = 0; }

And now, we will create our decorators:

class Logger { name: string; constructor(name: string) { this.name = name; } log(msg: string) { console.log(`${this.name} is logging: ${msg}`); } } function ElectricVehicle<T extends { new (...args: any[]): {} }>( constructor: T ) { return class extends constructor { batteryPercentage = 100; }; } function Logging<T extends { new (...args: any[]): {} }>(constructor: T) { return class extends constructor { logger = new Logger(constructor.name); }; }

In above example, we see the following:

  • A Logger class
  • A ElectricVehicle decorator function
  • A Logging decorator function

Logger class is a simple class which will basically have one function for logging, it will be provided by the name of the class in the decorator function.

The ElectricVehicle will by default, always add 100 to the batteryPercentage. And even though we set it to 0 in the Tesla class, the decorator value will override it.

Example:

const myTesla = new Tesla(); console.log(myTesla.batteryPercentage); // This will print 100

Our Logging decorator will add a new property logger to the class which are being wrapped with @Logging. It will provide the constructor name to the Logger class.

Example of an object of the class Tesla:

const myTesla = new Tesla(); console.log(myTesla); // Tesla {batteryPercentage: 100, logger: Logger}

Let's add some method decorators

All right, now we will continue with method decorators and see how we can use them all together.

Start with creating a function drive() in Tesla class and give it some simply logic:

public drive(requestedPercentage: number) { this.batteryPercentage = this.batteryPercentage - requestedPercentage; }

So, here we are substracting the batteryPercentage with the requested amount of percentage. Let's see how we can use decorators to prevent the batteryPercentage to getting below 0.

Let's create a method decorator and give it some logic:

const RangeChecker = (minPercentage: number) => (target: Object, propertyKey: string, descriptor: any) => { const originalMethod = descriptor.value; descriptor.value = function (requestedPercentage: number, ...args: any[]) { const percentageAfterDrive = this.batteryPercentage - requestedPercentage; if (percentageAfterDrive > minPercentage) { originalMethod.apply(this, [...args, requestedPercentage]); } else { console.error( `Not enough battery. Wanted to use ${requestedPercentage}%. But only have ${this.batteryPercentage}% left. Minimum is: ${minPercentage}%` ); } }; return descriptor; };

In this example, we are using decorator factory pattern. We basically want to provide the decorator with some initial value, which we will later use to check the percentage. So the value minPercentage can be different depending on the method we are applying it to.

Usage:

@RangeChecker(10) public drive(requestedPercentage: number) { this.batteryPercentage = this.batteryPercentage - requestedPercentage; }

So now our RangeChecker will make sure we always have at least 10 percent left in battery.

And now we also want to use our Logging functionality that we applied, so let's create a Log method decorator, and we will check how we can use the Logger class decorator in our method decorator.

Let's create our Log decorator:

interface LogDescriptor extends PropertyDescriptor { logger?: Logger; } function Log(target: any, propertyKey: string, descriptor: LogDescriptor) { const originalMethod = descriptor.value; descriptor.value = function (...args: any[]) { this.logger?.log(`Calling function ${propertyKey} with args: ${args}`); const result = originalMethod.apply(this, args); return result; }; return descriptor; }

All right, so what we are doing here, is basically using the logger property, that our Logger class decorator was applying to the class it was wrapping, and logging the function name and arguments for the method we are wrapping it with.

Example usage:

@Log @RangeChecker(10) public drive(requestedPercentage: number) { this.batteryPercentage = this.batteryPercentage - requestedPercentage; }

Cool. So now our full Tesla class looks like this:

@ElectricVehicle @Logging export class Tesla { batteryPercentage: number = 0; @Log @RangeChecker(10) public drive(requestedPercentage: number) { this.batteryPercentage = this.batteryPercentage - requestedPercentage; } }

Let's try it out

const myTesla = new Tesla(); myTesla.drive(70); // Tesla is logging: Calling function drive with args: 70 (Log decorator) myTesla.drive(50); // Tesla is logging: Calling function drive with args: 50 (Log decorator) // Error: Not enough battery. Wanted to use 50%. But only have 30% left. Minimum is: 10% (RangeChecker decorator)

Summary

There we have it folks, Decorators in TypeScript.

Are they useful? I would say that you can survive fine without them, but they do provide cool possibilities for us as developers to be creative and enhance our code. Also, e.g Angular are using decorators in their framework, like the @NgModule, @Component and @Input() for example. It can always be good to know what is going on underneath the hood when writing code, not to mention when debugging errors.

Hope you enjoyed it!

signatureFri Mar 24 2023
See all our articles