Wouter Vandenputte
Wouter Vandenputte

Reputation: 526

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.

Upvotes: 1

Views: 1606

Answers (2)

Alexander
Alexander

Reputation: 1

I used the accepted answer, but also had to add a .catch() invocation after .then() to avoid throwing exceptions into my logger.

Upvotes: 0

Eldar
Eldar

Reputation: 10790

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.

Upvotes: 3

Related Questions