+7

🌸Understanding the Power of the Decorator Pattern in Typescript🌸

JavaScript is a great way to make apps on almost any platform. TypeScript is a way to make JavaScript even better. It helps make sure that the code you write is correct and it adds some extra features that JavaScript doesn't have yet.

What are decorators?

Decorators are a pattern in programming that let you wrap something to change its behavior. It's not a new thing - many programming languages like Python, Java, and C# have it. JavaScript has proposed this feature, but TypeScript's decorator feature is different because it's a strongly typed language, so you can access extra information to do cool stuff like runtime type-assertion and dependency injection.

Types of decorators

Decorators are like special labels that you can put on classes and their parts, like methods and properties. Here are some examples of what they look like.

Class decorator

When you add a special feature to a class, the first thing you get is the constructor of the class.

const classDecorator = (target: Function) => {
  // do something with your class
}

@classDecorator
class Rocket {}

If you want to change something about the class, you can make a new class that is based on the old one, and then change the parts you want to be different.

const addFuelToRocket = (target: Function) => {
  return class extends target {
    fuel = 100
  }
}

@addFuelToRocket
class Rocket {}

Your Rocket class now has a fuel property that starts with a value of 100.

const rocket = new Rocket()
console.log((rocket).fuel) // 100

Method decorator

When you use a decorator, you can attach it to a class method. This means that when you use the method, you will get three pieces of information: target, propertyKey, and descriptor.

const myDecorator = (target: Object, propertyKey: string, descriptor: PropertyDescriptor) =>  {
  // do something with your method
}

class Rocket {
  @myDecorator
  launch() {
    console.log("Launching rocket in 3... 2... 1... 🚀")
  }
}

The Rocket class has a special way of changing how a method works. You can give it a name and some instructions about how it should work. This can help you make the method do more than it normally would.

Property decorator

The method decorator and the parameter decorator are similar in that they both give you two things: target and propertyKey. The only difference is that the parameter decorator does not give you the property descriptor.

const propertyDecorator = (target: Object, propertyKey: string) => {
  // do something with your property
}

If you want to learn more about decorators in TypeScript, you can look it up in the TypeScript documents.

Use cases for TypeScript decorators

Now that we know what decorators are and how to use them, let's look at how they can help us with certain tasks.

Calculate execution time

If you want to know how fast your application is running, you can create something called a decorator that will measure how long it takes for a function to run and then show you the time on the screen.

class Rocket {
  @measure
  launch() {
    console.log("Launching in 3... 2... 1... 🚀");
  }
}

The Rocket class has a special way to start it. To figure out how long it takes to start the Rocket, you can use the measure decorator.

import { performance } from "perf_hooks";

const measure = (
  target: Object,
  propertyKey: string,
  descriptor: PropertyDescriptor
) => {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args) {
    const start = performance.now();
    const result = originalMethod.apply(this, args);
    const finish = performance.now();
    console.log(`Execution time: ${finish - start} milliseconds`);
    return result;
  };

  return descriptor;
};

We are going to use something called the Performance Hooks API from the Node.js standard library to measure how long it takes to launch a Rocket. We will create a new Rocket and then call the launch method. The measure decorator will replace the original launch method with a new one that will measure how long it takes to launch the Rocket and then log the time to the console.

const rocket = new Rocket();
rocket.launch();

You will get the following result.

Launching in 3... 2... 1... 🚀
Execution time: 1.0407989993691444 milliseconds

Decorator factory

Decorator factory is a way to make your decorations do different things in different situations. It's like a factory that makes decorations, but you can give it instructions to make the decorations do different things. For example, you can tell the factory to make a decoration that looks a certain way or does something special.

const changeValue = (value) => (target: Object, propertyKey: string) => {
  Object.defineProperty(target, propertyKey, { value });
};

The changeValue function makes a special thing called a decorator. This decorator changes the value of something based on what you tell it to do from your factory.

class Rocket {
  @changeValue(100)
  fuel = 50
}

const rocket = new Rocket()
console.log(rocket.fuel) // 100

If you connect the decorator factory to the fuel, the amount of fuel will be 100.

Automatic error guard

Let's use what we have learned to figure out how to solve a problem in the real world.

class Rocket {
  fuel = 50;

  launchToMars() {
    console.log("Launching to Mars in 3... 2... 1... 🚀");
  }
}

If you have a special type of spaceship called a Rocket, it needs to have enough fuel to get to Mars. We can make a special thing called a decorator that will make sure the Rocket has enough fuel before it takes off.

const minimumFuel = (fuel: number) => (
  target: Object,
  propertyKey: string,
  descriptor: PropertyDescriptor
) => {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args) {
    if (this.fuel > fuel) {
      originalMethod.apply(this, args);
    } else {
      console.log("Not enough fuel!");
    }
  };

  return descriptor;
}; 

The minimumFuel is like a special tool that helps you check how much fuel is needed to launch a rocket. You can use it to make sure the rocket has enough fuel before it takes off.

class Rocket {
  fuel = 50;

  @minimumFuel(100)
  launchToMars() {
    console.log("Launching to Mars in 3... 2... 1... 🚀");
  }
}

If you try to make the rocket go to Mars, it won't work because it doesn't have enough fuel.

const rocket = new Rocket()
rocket.launchToMars()

Not enough fuel!

This means that you can use the same code to make a new method, but just change the number you are checking for. So if you want to make a new method to launch the rocket to the moon, you can use the same code but just change the number you are checking for from 25 to something else.

class Rocket {
  fuel = 50;

  @minimumFuel(100)
  launchToMars() {
    console.log("Launching to Mars in 3... 2... 1... 🚀");
  }

  @minimumFuel(25)
  launchToMoon() {
    console.log("Launching to Moon in 3... 2... 1... 🚀")
  }
}

Now, this rocket can be launched to the moon.

const rocket = new Rocket()
rocket.launchToMoon()

Launching to Moon in 3... 2... 1... 🚀

This type of decorator can help you decide if someone is allowed to see special information or not. It can help you make sure that only the right people can see it.

Conclusion

Sometimes you don't need to make your own decorations. There are already libraries and frameworks like TypeORM and Angular that have all the decorations you need. But it's still a good idea to understand what's happening behind the scenes. It might even give you ideas for making your own TypeScript framework.

Mình hy vọng bạn thích bài viết này và học thêm được điều gì đó mới.

Donate mình một ly cafe hoặc 1 cây bút bi để mình có thêm động lực cho ra nhiều bài viết hay và chất lượng hơn trong tương lai nhé. À mà nếu bạn có bất kỳ câu hỏi nào thì đừng ngại comment hoặc liên hệ mình qua: Zalo - 0374226770 hoặc Facebook. Mình xin cảm ơn.

Momo: NGUYỄN ANH TUẤN - 0374226770

TPBank: NGUYỄN ANH TUẤN - 0374226770 (hoặc 01681423001)

image.png


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí