typescriptjavascript-decorators

Decorator in Typescript to return timing of function


Usecase: I want to get how much time a function takes to execute in typescript. I want to use decorator for this purpose. I want the decorator should return the time so that (I can further use it), not just print it.
For eg :

export function createTimestamps(message: string) {
  return function (target: any, name: string, descriptor: PropertyDescriptor) {
    const method = descriptor.value;
    descriptor.value = async function () {
      const startTime = new Date(Date.now());
      console.log(
        `${message} started at: ${startTime.toLocaleString("en-GB")}`
      );
      await method.apply(this);
      const endTime = new Date(Date.now());
      console.log(
        `${message} completed at: ${endTime.toLocaleString("en-GB")}`
      );
      console.log(
        `${message} took ${
          endTime.getTime() - startTime.getTime()
        }ms to complete.`
      );
    };
  };
}

If I use above function as decorator , then I would like the decorator to return "endTime.getTime() - startTime.getTime()" , so that I can further use it .

@creaTimestamp
async newfunc():string{
 return "typescript";
}

Now when I call above function await newfunc(). Can I get execution time value as well along with the string it is returning ?

Also I have many functions I want to avoid adding decorator on top of each one of them, and thus while calling them I want make sure that the decorator runs and returns the timing as well. Can someone point me to such library if exist ?

Can someone please share some insight in above scenarios, I am very new to decorators. Thanks!


Solution

  • Returning additional data is not possible with typescript decorators.

    The main problem is that a typescript decorator cannot change the return type of a function. And a function can only return one value.

    So, why is that important?

    For example:

    class Foo {
      async newfunc(): string {
        return "typescript";
      }
    }
    
    const lang = await new Foo().newFunc()
    console.log(`${lang} is a cool language`)
    

    In this example lang is a string, and your program expects it to be a string. If you then put the decorator on this function, and you expect that return a string as well as timing information, you'd have to return something like:

    { result: returnValueOfFunctionHere, elapsed: elapsedMsHere }
    

    But that's not a string anymore. Now you'd have to drill into the result property to get that string. That means you've changed the return type of your function by applying the decorator. And that's not allowed at the moment.

    This is the reason that example logs the info to the console rather than returning it.


    But as @Papooch suggests, you can stash the elapsed time in metadata via reflect-metadata. You'd store this on a unique metadata key on the function being measured.

    reflect-metadata is a useful package for things like this. Read more on how to use it here.

    import 'reflect-metadata'
    
    // Create a key to store the metadata under. This should be a symbol.
    const lastCallElapsedKey = Symbol('lastCallElapsedKey')
    
    function createTimestamps(message: string) {
      return function (target: any, name: string, descriptor: PropertyDescriptor) {
        const method = descriptor.value;
    
        descriptor.value = async function () {
          const startTime = new Date(Date.now());      
          const result = await method.apply(this);
          const endTime = new Date(Date.now());
          const elpased = endTime.getTime() - startTime.getTime()
    
          // Write the elpased time to the new function's metadata.
          Reflect.defineMetadata(lastCallElapsedKey, elpased, descriptor.value)
          
          return result
        };
      };
    }
    
    // Get the elapsed time from the metadata.
    function getElapsed(fn: (...args: unknown[]) => unknown): number | undefined {
        return Reflect.getMetadata(lastCallElapsedKey, fn)
    }
    
    class Foo {
        // Emulate an async call
        @createTimestamps("test")
        async newfunc(): Promise<string> {
            return new Promise(resolve => {
                setTimeout(() => resolve('typescript'), 250)
            })
        }
    }
    
    async function test() {
        const foo = new Foo()
        console.log(await foo.newfunc())
        console.log(getElapsed(foo.newfunc)) // 250
    }
    test()
    

    Playground