typescripttypescript-decorator

Typescript Class Decorator for all functions


This question has been asked already for different languages, mostly Python but I need the same for my Typescript classes.

Suppose I have this class

class MyClass() {

  private _foo: string = 'abc';

  public printX() : void {
    console.log('x');
  }

  public add(a: number, b:number) : number {
    return a + b;
  }

  public get foo() : string {
    return this._foo;
  }
}

How can I now decorate my class

@TimeMethods()
class MyClass() { .... }

Sucht that I can time the exeuction of all functions. Such that the functions printX and add(a,b) are logged with their execution time, but the variables and getters _foo and get foo respectively are not.

I already wrote a decorator to time individual functions

export function TimeDebug(): MethodDecorator {
  return function (target: object, key: string | symbol, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      const start = new Date();

      originalMethod.apply(this, args);
      console.log(`Execution of ${key.toString()} took ${new Date().getTime() - start.getTime()} ms.`);
    };

    return descriptor;
  };
}

And I would like this to be automatically applied to each function in a class if the class is decorated as such.


Solution

  • Here is the final version of the mentioned interceptor. The summary of it: acquire all properties, intercept functions, skip constructor, and apply a special treatment for the properties.

    function intercept<T extends { new(...args: any[]): {} }>(target: T) {
      const properties = Object.getOwnPropertyDescriptors(target.prototype);
    
      for (const name in properties) {
        const prop = properties[name];
        if (typeof target.prototype[name] === "function") {
          if (name === "constructor") continue;
          const currentMethod = target.prototype[name]
    
          target.prototype[name] = (...args: any[]) => {
            // bind the context to the real instance
            const result = currentMethod.call(target.prototype, ...args)
            const start = Date.now()
            if (result instanceof Promise) {
              result.then((r) => {
                const end = Date.now()
    
                console.log("executed", name, "in", end - start);
                return r;
              })
            } else {
              const end = Date.now()
              console.log("executed", name, "in", end - start);
            }
            return result;
          }
    
          continue;
        };
        const innerGet = prop!.get;
        const innerSet = prop!.set;
        if (!prop.writable) {
          const propDef = {} as any;
          if (innerGet !== undefined) {
            console.log("getter injected", name)
            propDef.get = () => {
              console.log("intercepted prop getter", name);
              // the special treatment is here you need to bind the context of the original getter function.
              // Because it is unbound in the property definition.
              return innerGet.call(target.prototype);
            }
          }
    
          if (innerSet !== undefined) {
            console.log("setter injected", name)
            propDef.set = (val: any) => {
              console.log("intercepted prop setter", name, val);
              // Bind the context
              innerSet.call(target.prototype, val)
            }
          }
          Object.defineProperty(target.prototype, name, propDef);
        }
      }
    }
    
    

    See it in action

    Edit As @CaTS mentioned using an async interceptor breaks the original sync function as it starts to return Promise by default. If your environment uses non-native promises you should see the SO answer mentioned in the comments.