pwd > ninjaPixel/v2/man-drawer/typescript-decorators

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.