Learn to use Decorators once and for all
Ivan Zaldivar
Posted on October 12, 2021
Hi Developers! How are you? I hope everyone is well. A few weeks ago I begin to investigate in depth how decorators really work. It's not that I've never worked with Decorators, of course I have, especially Angular and Nest, but I never bothered to understand how they work. So I decided to get down to let's do it. And the truth is, it is easier than it seems.
What are @Decorators?
A decorator is nothing more than a "typical" Javascript/Typescript function what to add additional functionalities to a declaration. When we say declaration, we mean classes, methods, parameters, accessor (getter) and properties. Now, we are going to explain each one of them.
Note: You will have noticed, that at no time did we mention functions, this is so because the main problem is dealing with the elevation of functions. Any attempt to wrap one function in another function breaks the hoist.
In the decorators we find two types, basic decorators and factories. How do we differentiate each one? I show it to you with the following image.
When we use factory it is because we need to pass some parameters, instead we use predefined values in the other.
Get started!
What we will do at this point is to create a class that will help us understand how decorators work.
interface Note {
id: number;
title: string;
text: string;
}
class NoteUI{
notes: Note[] = [
{
id: 1,
title: "Docker",
text: "🐳 Beautiful platform."
},
{
id: 2,
title: "Firebase",
text: "🔥 Beautiful platform"
}
]
}
Perfect this is all we need.
Class Decorators.
A Class Decorator is declared just before a class declaration. The class decorator is applied to the constructor of the class and can be used to observe, modify, or replace a class definition.
Important Announcement Should you choose to return a new constructor function, you must take care to maintain the original prototype. The logic that applies decorators at runtime will not do this for you.
Some considerations
- The expression for the class decorator will be called as a function at runtime, with the constructor of the decorated class as its only argument.
- If the class decorator returns a value, it will replace the class declaration with the provided constructor function.
Example
We build the decorator that adds a property of type Map
and two methods, one for insertion and the other for deletion.
function Store() {
return function (constructor: Function) {
// Set new entities in the class.
constructor.prototype.entities = new Map();
// Add method for insert new entities.
constructor.prototype.addOne = function (entity: Record<string, string>) {
constructor.prototype.entities.set(entity.id, entity);
}
// Add method for remove entities.
constructor.prototype.removeOne = function (id: string | number) {
constructor.prototype.entities.delete(id);
}
}
}
Now, we add decorator in the class.
// Add decorator in the class.
@Store()
class NoteStore {}
const noteStore = new NoteStore();
// Add new entity.
noteStore.addOne({id:1, name:"Ivan"});
// Show all entities.
noteStore.entities.forEach(console.log);
// Delete a entity.
noteStore.removeOne(1);
// Show all entities.
noteStore.entities.forEach(console.log);
Method Decorators.
These decorators are characterized because they are declared just before our method. You have to understand that the declaration will be executed at runtime and receives the following parameters.
-
target
: The first parameter contains the class where this method lives. -
key
: Name of the member (method) -
descriptor
: The Property Descriptor for the member.
Taking the previous class (NoteController) we add the following method.
// Create method decorator.
function Confirm(message: string): any {
return function (
target: Object,
key: string | symbol,
descriptor: PropertyDescriptor
) {
let original = descriptor.value;
descriptor.value = function (...args: any[]) {
if (confirm(message)) return original.apply(this, args);
return null;
};
};
}
class NoteUI {
@Confirm("This item will be removed. Are you sure?")
remove(element: HTMLElement | null): void {
element?.remove();
}
}
Explanation: The function of this decorator is to show a confirmation window with the message that we have passed as a parameter, if the user accepts, it removes the element, otherwise, nothing happens.
Parameter Decorators.
A Parameter Decorator is declared just before a parameter declaration. The parameter decorator is applied to the function for a class constructor or method declaration.
Considerations
- A parameter decorator cannot be used in a declaration file, an overload, or in any other ambient context (such as in a declare class).
- A parameter decorator can only be used to observe that a parameter has been declared on a method. So the return value of the parameter decorator is ignored.
In this case I did not find an example that was entirely useful, so I did not add anything. I'm sorry buddy. But if you have any good ones, feel free to comment on them, please.
Property Decorators.
A Property Decorator is declared just before a property declaration.
Receives the following parameters.
- Target: Either the constructor function of the class for a static member, or the prototype of the class for an instance member
- Key: property name in string format.
export type CapitalizeForm = "All" | "First";
export function Capitalize(option: CapitalizeForm = "First") {
return function (target: any, key: string | symbol) {
// Get value of the property.
let value = target[key];
// Assign getter.
const getter = function () {
return value;
};
// Capitalize first letter.
function capitalize(word: string): string {
return word[0].toUpperCase() + word.slice(1);
}
// Assign setter.
const setter = function (data: string) {
// Capitalize all first letter of a word by each space
if (option === "All") {
value = data
.split(" ")
.map((word) => capitalize(word))
.join(" ");
return;
}
// Capitalize only first letter.
value = capitalize(data);
};
// Define property.
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
};
}
class NoteUI {
@Capitalize()
message: string = "hi developers!";
}
const noteUI = new NoteUI();
// @Capitalize("First")
console.log(noteUI.message); // Hi developers!
// @Capitalize("All")
console.log(noteUI.message); // Hi Developers!
Note: A property decorator cannot be used in a declaration file, or in any other ambient context (such as in a declare class).
What this decorator does is capitalize a text string. I already know the first letter or all the content.
Accessor Decorators.
An Accessor Decorator is declared just before an accessor declaration. The accessor decorator is applied to the Property Descriptor for the accessor and can be used to observe, modify, or replace an accessor’s definitions. You have to understand that accessor is nothing more than a getter of our class.
Note: An accessor decorator cannot be used in a declaration file, or in any other ambient context (such as in a declare class).
Example
// Create a decorator.
export function Directory() {
return function (
target: any,
key: string | symbol,
descriptor: PropertyDescriptor
) {
const original = descriptor.get;
descriptor.get = function () {
let result = original?.apply(this);
result = Object.assign(
{},
...result.map((item: any) => ({ [item.id]: item }))
);
return result;
};
return descriptor;
};
}
class NoteUI {
@Directory()
get items() {
return this.notes;
}
}
const noteUI = new NoteUI();
console.log(note.items)
// { 1: { id: 1, title: "A title", text: "A description" } }
Explanation: This decorator have the responsibility of transform an array to object or directory.
Well, this is the end of this article. There is still a long way to go to be a pro with decorators, but it is only a matter of practice. Any questions or suggestions are welcome in your comments. I hope I have helped you friend.
Follow me on social networks.
- 🎉 Twitter: https://twitter.com/ToSatn2
- 💡 Github: https://github.com/IvanZM123
Posted on October 12, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.