Decorators are an exciting and upcoming feature in TypeScript 5.0 that will enable developers to customize classes and their members in a reusable way. This article will introduce decorators and show how they can be used to improve code.
Understanding Decorators
Let’s start by looking at an example. Consider the following code:
typescriptCopy codeclass Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ron");
p.greet();
Here, we have a simple class called Person with a single method called greet. It’s easy to read, but let’s imagine that greet is much more complicated – maybe it does some asynchronous logic, it’s recursive, it has side effects, etc. Regardless of what kind of ball-of-mud you’re imagining, let’s say you throw in some console.log calls to help debug greet.
typescriptCopy codeclass Person {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log("LOG: Entering method.");
console.log(`Hello, my name is ${this.name}.`);
console.log("LOG: Exiting method.")
}
}
This pattern is fairly common. It sure would be nice if there was a way we could do this for every method!
This is where decorators come in. We can write a function called loggedMethod that looks like the following:
typescriptCopy codefunction loggedMethod(originalMethod: any, _context: any) {
function replacementMethod(this: any, ...args: any[]) {
console.log("LOG: Entering method.")
const result = originalMethod.call(this, ...args);
console.log("LOG: Exiting method.")
return result;
}
return replacementMethod;
}
Notice that loggedMethod takes the original method (originalMethod) and returns a function that logs an “Entering…” message, passes along this and all of its arguments to the original method, logs an “Exiting…” message, and returns whatever the original method returned.
Using Decorators
Now we can use loggedMethod to decorate the method greet:
typescriptCopy codeclass Person {
name: string;
constructor(name: string) {
this.name = name;
}
@loggedMethod
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ron");
p.greet();
We just used loggedMethod as a decorator above greet – and notice that we wrote it as @loggedMethod. When we did that, it got called with the method target and a context object. Because loggedMethod returned a new function, that function replaced the original definition of greet.
Understanding the Context Object
We didn’t mention it yet, but loggedMethod was defined with a second parameter. It’s called a “context object,” and it has some useful information about how the decorated method was declared – like whether it was a #private member, or static, or what the name of the method was.
A decorator that uses addInitializer to call bind in the constructor for us would look like this:
function autobind(_: any, _2: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
return {
configurable: true,
get() {
const boundFn = originalMethod.bind(this);
Object.defineProperty(this, descriptor.key, {
value: boundFn,
configurable: true,
writable: true,
});
return boundFn;
},
};
}
Here, we’re ignoring the first two parameters (the class and the method name) since we don’t need them. The third parameter is the property descriptor, which has a lot of information about how the property was defined, including the value of the method itself.
We then return a new property descriptor with a get accessor. When the method is accessed (either by calling it or getting its value), we bind the method to the instance of the class and return it. We then replace the original method with the bound function.
This decorator allows us to write classes like this:
class Person { name: string; constructor(name: string) { this.name = name; }
typescriptCopy code@autobind
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
const p = new Person("Ron"); const greet = p.greet;
// We don't want this to fail! greet();
Now, when we call greet as a standalone function, it won’t fail because it’s already bound to the instance of Person.
Decorators are a powerful feature of TypeScript 5.0 that allow us to customize classes and their members in a reusable way. By using decorators, we can separate the concerns of our code and make it more maintainable.
In this article, we’ve seen how to use decorators to log method calls and bind methods to instances of classes. We’ve also seen how decorators can be used to add metadata to classes and methods.
While decorators are still an upcoming feature of TypeScript 5.0, they are already available in TypeScript and can be used in projects that target modern browsers or Node.js. As decorators become more widely adopted, they will likely become an indispensable tool in the JavaScript developer’s toolkit.
Thank you for reading this article, and I hope it has given you a better understanding of decorators in TypeScript 5.0.