TypeScript decorators
Published
Introduced in TypeScript v5.0 (see the great Microsoft release notes), decorators are an upcoming ECMAScript feature, they enable us to reuse code throughout class members.
Imagine that we want to log the execution time of a particular class method:
export default class AwesomeClass {
constructor() {}
doSomething() {
const startTime = new Date();
for (let i = 0; i < 9999999; i++) {
// simulating a slow process
}
const endTime = new Date();
const duration = endTime.getTime() - startTime.getTime();
console.info("doSomething took:", duration, "ms");
}
}
Simple enough, but how can we make this DRY and easily apply it to other class methods? This is where decorators come in, allowing us to change the above class to just:
export default class AwesomeClass {
constructor() {}
@logPerformance
doSomething() {
for (let i = 0; i < 9999999; i++) {
// simulating a slow process
}
}
}
Here, @logPerformance
applies the logPerformance
decorator to the doSomething
class method. In it's simplest form,
the logPerformance
decorator would look like this:
function logPerformance<This, Args extends any[], Return>(
originalMethod: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<
This,
(this: This, ...args: Args) => Return
>,
) {
function replacementMethod(this: any, ...args: Args) {
const methodName = String(context.name);
const startTime = new Date();
const result = originalMethod.call(this, ...args);
const endTime = new Date();
const duration = endTime.getTime() - startTime.getTime();
console.info(`"${methodName}" took: ${duration}ms`);
return result;
}
return replacementMethod;
}
A decorator function returns a new function which accepts this
and an unspecified number of arguments as its
parameters. When the decorator function is called, it's called with the original method and a context object. We can use
the context object to extract the name of the method, which is quite useful as you can see in the above example as it
allows us to include the method name in the logging message (fyi the context object also includes other information like
if it's a private or static method).
Of course, the most important part of all of this is the fact that we call the original method.
Note that we execute originalMethod.call(this, ...args)
rather than originalMethod(...args)
because the original
method will most likely be referencing class properties and will hence need to be bound to the correct this
.