Welcome 🤗 and first of all, I should mention that this article requires a background on TypeScript, TypeScript Generics and Object Oriented Programming.
Now, what is a decorator? 🤔 In the real life, decorating something means to change its style of or its functionality. Have you ever changed the design of your room or purchased for an extensible sofa that can be converted into a bed 😅?
In the world of programming, a decorator is a design pattern, it is just a function that wraps another function to add or modify some features of it.
function decoratorFunc(fn) {
//some code
fn();
//some other code
}
The above function decoratorFunc
is a decorator in its simplest form. You can pass any function and add more code to be executed either before or after the main function fn
.
So, _what are the types of decoratorsin TypeScript?
_ 🤔
- class decorator
- method decorator
- accessor decorator
- property decorator
- parameter decorator
Which means that a decorator function can be attached to any of the above entities.
In this article we will dive deep into class decorator.
To apply decorators they must be prefixed with the @ symbol and placed immediately before the construct to be decorated.
We now know that the decorator is just a function, so to create a Class Decorator in TypeScript we just write a function, but it must have one argument which is the class itself. 🤓
function classDecorator(target: Function) {
console.log(`This is to decorate class ${target.name}`);
}
This is the simplest class decorator you will ever see 😂.
The target
parameter is the class constructor "the class that the decorator will wrap".
So let's create a class to make the example clear.
class User {
constructor(
public fName: string,
public lName: string,
) {}
fullName(): string {
return `${this.fName} ${this.lName}`;
}
}
To apply the classDecorator
decorator to the User
class we should write
@classDecorator
class User {
constructor(
public fName: string,
public lName: string,
) {}
fullName(): string {
return `${this.fName} ${this.lName}`;
}
}
Because we prefix classDecorator
with @ and place it before the User
class , class User
will be passed to the classDecorator
.
If you try to execute it, you should see the logs we print inside the decorator.
But it is important to know that the decorator will get executed on the runtime not on the class instantiation.
And voila, you create your first class decorator 🥳
What we have just written is a Decorator Function, what if we need to pass another argument to this function?!🤔 and it should take only one which is the class.
In this scenario we will need a Decorator Factory Function again, just a function the only difference that it can take any number of arguments and it returns the decorator function itself 👇
function logCustomMessage(message: string) {
return function (target: Function) {
console.log(message)
}
}
and then we pass the argument on calling it. 👇
@logCustomMessage('This is a custom message !')
class User {
constructor(
public fName: string,
public lName: string,
) {}
fullName(): string {
return `${this.fName} ${this.lName}`;
}
}
Hmmm, can a decorator function return values? 🤔
Good question 😅
From the official documentation 👇
If the class decorator returns a value, it will replace the class declaration with the provided constructor function.
A decorator function can only return classes
👉 Use Cases 👈
Let's introduce two examples from which we can take advantage of the decorators and know how to return a class.
Assume we're building a system for a company, so we have structured the main classes, but we need to apply some features to this class that doesn't deeply relate to the logic of the class itself.
Example 1:
We create a Department
class that has some members including the code of each department. This code is a unique identifier for each department and must be unique. So, we need to apply a validation feature away from the class logic to handle this case.
The decorator we can write to validate the code. 👇
function uniqueCode<C extends new (...args: any[]) => {}>(target: C) {
return class Mixin extends target{
static codes: string[] = [];
// the following constructor will get executed on creating object of the original class
constructor(...args: any[]) {
super(args);
const code = args[1];
if (Mixin.codes.includes(code)) {
throw new Error(`Department code must be unique!`);
}
Mixin.codes.push(code);
}
};
}
Department class 👇
class Department {
constructor(public name: string, public code: string) {}
}
And to apply the decorator on the class 👇
@uniqueCode
class Department {
constructor(public name: string, public code: string) {}
}
The uniqueCode
decorator piece by piece 👇
The function definition: we specify type of the argument
target
, that it will be custom typeC
which is restricted tonew (...args: any[]) => {}
. This means that it should be aconstructor
function that we call usingnew
keyword, and thisconstructor
function can have any numbers of arguments, then after calling it, it will return finally an object.The function body: we just return a new class that extends the original class, to add more features, and there is the place we implement the validation feature.
Now if you try to execute 👇
const hrDep = new Department("HR", "H259");
const marketingDep = new Department("Marketing", "H259");
it will throw an error.
NOTE THAT the class we returned from the decorator will replace the original one. Try to execute console.log(Department.toString())
before and after applying the decorator to see the difference.
Example 2:
What if we need to reuse this decorator to be applied to the Employee
class as well 🤔.
The dynamic information we need is to know the index of the code attribute in each class we will apply the decorator to.
By rewriting uniqueCode
decorator and using Decorator Factory Function instead, we can pass extra arguments.
function uniqueCode(codeIndex: number) {
return function <C extends new (...args: any[]) => {}>(target: C) {
return class Mixin extends target {
static codes: string[] = [];
constructor(...args: any[]) {
super(args);
const code = args[codeIndex];
if (Mixin.codes.includes(code)) {
throw new Error(`${target.name} code must be unique!`);
}
Mixin.codes.push(code);
}
};
};
}
Now we can pass the index of the code
attribute according to its definition in the original class.
Applying the dynamic decorator to any class that has code attribute 👇
// 1 is the index of code in the constructor parameters
@uniqueCode(1)
class Department {
constructor(public name: string, public code: string) {}
info() {
return {
name: this.name,
code: this.code,
};
}
}
// 2 is the index of code in the constructor parameters
@uniqueCode(2)
class Employee {
constructor(
public name: string,
public salary: number,
public code: string
) {}
}
And finally we are at the end 😄. You can implement great ideas using decorators and be safe away from the original logic of the class. Try it now 🙂
Top comments (1)
Amazing 😍