Start Implementing your own Typescript Method Decorators
What is a Decorator?
It is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors (reference).
When we are using Typescript Method Decorators, they are higher-order functions that help us change method behavior or do something with the arguments.
Typescript method decorator definition : method decorator can be used to observe, modify, or replace a method definition (reference)
Now, let’s see how we can define a simple method decorator.
Setup
In order to run the Typescript code, we need to compile them using the Typescript compiler.
We need a tsconfig.json file :
{
"compilerOptions": {
"experimentalDecorators": true,
"target": "ES5"
}
}
We have to enable the experimentalDecorators. Also, the target should not be less than ES5.
If you do not want to use a tsconfig file you can pass these options directly:
tsc --experimentalDecorators // If you installed tsc globaly
npx tsc --experimentalDecorators // If you installed tsc in your current directory
Now by running tsc in the current directory, the typescript files will compile to javascript files and we can run them using Node.
Define a method decorator
The general structure of a method decorator in typescript is like this:
function DecoratorName(value:any) {
return function (
target: Object,
key: string | symbol,
descriptor: PropertyDescriptor
) {
console.log(key);
return descriptor;
};
}
class Test {
@DecoratorName({ someData: 1 })
myMethodInClass() {}
}
Passing arguments to the decorator
We can pass a variable to the decorator and we have access to it inside the decorator. we passed an object in line 13 and it is going to be the argument in line 1.
The return function has 3 arguments.
* target : contains our target(the function, class, property that we want to do something with it)
* key : In method decorator, it is the name of the method.
* descriptor : It is a property descriptor and it gives us the child function(method) in descriptor.value and we can change the behaviors of the method.
If you are not familiar with property descriptor this article will help you understand it but in plain language, it is something that helps us to define properties for an object and we have some options or we can set constraints on that property.
In the first example, we want to do something really simple we want to log the name of the function each time someone calls it.
Example1: Function name logger
function FunctionNameLogger() {
return function (
target: Object,
key: string | symbol,
descriptor: PropertyDescriptor
) {
console.log(key);
return descriptor;
};
}
class Test {
@FunctionNameLogger()
Sum(a: number, b: number): number {
return a + b;
}
}
const instance: Test = new Test();
instance.Sum(1, 4);
All we did in this decorator is logging the key in line 7 .
Important : You cannot use a decorator outside a class(Decorators have meaning when we are using classes)
Now if I run this file using this command: npx tsc; node loggerDecorator.js
I can see the name of the function in my console: Sum .
Example 2: Check Duplicate User
Now let’s write something more real and see how we can use the descriptor. We want to write a decorator that checks if the user email is duplicated or not. We can implement something like this inside our method but if we want to use it more than once in our code, a decorator to check this will be helpful and have to know that this is just an imaginary example to show you how it works(There are zillion ways to implement this better than example :) don’t get too bogged down).
const userEmails = ["p.shaddel@gmail.com", "test@gmail.com"];
function CheckDuplicateUser() {
return function (
target: Object,
key: string | symbol,
descriptor: PropertyDescriptor
) {
const childFunction = descriptor.value;
descriptor.value = function (...args: any[]) {
const emails = userEmails;
if (emails.indexOf(args[0].email) !== -1) {
return null;
} else {
return childFunction.apply(this, args);
}
};
return descriptor;
};
}
class UserService {
@CheckDuplicateUser()
createUser(user: any): void {
console.log("creating user in database:", user); // we have to insert user to the database.
}
}
const userService: UserService = new UserService();
userService.createUser({ name: "Poorshad", email: "p.shaddel@gmail.com" });
userService.createUser({ name: "John", email: "John.smith@gmail.com" });
Let’s see what is happening here line by line:
line 1: We have a const userEmails which contains a sample array of user emails. This is supposed to come from the database.
line 8: If the user is not duplicated, we want to run the child function so we need to store the original method in a variable.
line 9: This is how we can change the function. we can access the arguments of the child function here and since we may have multiple arguments we need to extract the arguments like an array(by using …).
line 11: we are checking if the email is duplicated or not. we expect the first argument to be the user object so to access it we have to use args[0] and we check that if the email exists or not using indexOfwhich is a method that returns -1 when the element does not exist in the array. If the email is duplicated we are going to return null so the method will not be called. If the user is not duplicated we have to execute the method so I returned childFunction.apply(this, args) and as a result, if the user is not duplicated, the method will execute with its arguments.
line 21: This is the user service class which has a method named createUser and we used our method decorator to check if the user is duplicated or not. If the user is duplicated we should not see something in the console and if the user is unique, the method should be called and as a result, we should see the log on the console.
line 28: We instantiated from the UserService class and after that, we are using the createUser method twice in lines 29–30. one of the emails is used before so we expect that the method createUser to be called only one time for the user with email: john.smith@gmail.com
This is the result on my console:
The code result in the console
Conclusion
Using method decorator is not that hard and if we are using typescript and object-oriented programming we can use decorators to change methods behavior. we have access to arguments of method and the arguments of the decorator and we can do something based on arguments.
Top comments (0)