Using a Typescript Class Decorator to Enhance Functionality

Can anyone help me implement a timing decorator for my Typescript classes similar to what is commonly used with Python?

Consider the following example 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;
  }
}

I want to decorate my class like so

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

In order to record the execution time of all functions. Specifically, I only want the functions printX and add(a,b) to be timed, while disregarding variables and getters _foo and get foo.

I have already created 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;
  };
}

How can I automatically apply this decorator to each function within a class that has been decorated as such?

Answer №1

Here is the completed version of the interceptor discussed earlier. In summary, it gathers all properties, intercepts functions, skips the constructor, and applies 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 actual 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);
    }
  }
}

View it in operation

Edit As @CaTS pointed out, using an async interceptor may affect the original sync function as it now returns a Promise by default. If your environment does not support native promises, refer to the SO answer mentioned in the comments.

Answer №2

After following the recommended solution, I found it necessary to include a .catch() statement following the .then() in order to prevent any exceptions from being logged.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Transfer all specified resources from one stack to another in AWS CDK

In the process of creating two stacks, I aim to reference the resources from the first stack, such as Lambda, API Gateway, and DynamoDB, in the second stack without hard coding all the resources using Stack Props. Please note: I do not want to use Stack Pr ...

How to set an already existing anonymous object to a property within the data property in VueJS

Help needed for a beginner question let myOptions: { chart: { height: 350, type: 'bar' }, colors: ["#800000"] }; let vueExample = new Vue({ el: '#example', components: { apexchart: VueApexCh ...

Tips for determining data type in TypeScript without using the 'instanceof' operator

Attempting to upgrade a basic custom Link component in React to TypeScript has been a challenge. Below is the initial code for the simple Link component written in JavaScript: import React from "react"; import { Link as RouterLink } from "r ...

What could be causing my NextAuthJS discord login to function in the test environment but fail to deploy in production on NextJS 13?

After attempting to deploy my NextJS application on Vercel, I encountered a problem with the AuthJS sign-in model. While running the program using npm run dev, everything works smoothly and I can log in to my Discord account linked to the developer portal& ...

How to dynamically set a background image using Ionic's ngStyle

I've been trying to set a background image on my card using ngStyle Take a look at my code below: <ion-slides slidesPerView="1" centeredSlides (ionSlideWillChange)= "slideChange($event)" [ngStyle]= "{'background-image': 'ur ...

Issue with Angular: Unable to locate a differ that supports the object '[object Object]' of type 'object'. NgFor is only compatible with binding to Iterables such as Arrays

As someone who is new to Angular, I am facing a challenge while working on my portfolio project. The issue arises when trying to receive a list of nested objects structured like this: "$id": "1", "data": { &quo ...

Export full module as a constructor function

Imagine having a nodejs module where requiring it gives you a constructor function. Here's an example: var Mod = require("modulename"); var mod = new Mod(); mod.method(); Now, I want to create a .d.ts declaration file that can be imported and utiliz ...

Can anyone assist me with creating a reusable component for `next-intl` that can be

Let me explain what I'm looking for. If you're not familiar with next-intl, it's a package designed to provide internationalization support for Next.js applications. The Issue: The developer of next-intl is working on a beta version that su ...

Alter the style type of a Next.js element dynamically

I am currently working on dynamically changing the color of an element based on the result of a function //Sample function if ("123".includes("5")) { color = "boldOrange" } else { let color = "boldGreen" } Within my CSS, I have two clas ...

Retrieve the input type corresponding to the name and return it as a string using string template literals

type ExtractKeyType<T extends string, K extends number> = `${T}.${K}`; type PathImpl<T, Key extends keyof T> = Key extends string ? T[Key] extends readonly unknown[] ? ExtractKeyType<Key, 0 | 1> : T[Key] extends Record<str ...

Creating an array with varying types for the first element and remaining elements

Trying to properly define an array structure like this: type HeadItem = { type: "Head" } type RestItem = { type: "Rest" } const myArray = [{ type: "Head" }, { type: "Rest" }, { type: "Rest" }] The number of rest elements can vary, but the first element ...

Exploring how to read class decorator files in a Node.js environment

I've developed a custom class decorator that extracts permissions for an Angular component. decorator.ts function extractPermissions(obj: { [key: 'read' | 'write' | 'update' | 'delete']: string }[]) { re ...

Exploring Typescript's conditional types and narrowing branches

When I use the following code snippet: type Identity <T extends string> = T; type MaybeString = string | undefined; type StringOrNever = MaybeString extends undefined ? never : Identity<MaybeString>; The compiler raises an error stating that ...

Absence of a connectivity pop-up within Ionic 2

Whenever the app loads, my "network check" should run automatically instead of waiting for the user to click on the Check Connection button. I tried putting it in a function but couldn't get it to work. Can anyone help me identify the syntax error in ...

Change the keys of the object in the return statement

Scenario Imagine a scenario where a method called matter is returning an object in the form of return {content, data} Issue There is a conflict when the method is called a second time, as it overwrites any previous variables that were set from the return ...

The inability to destructure the 'store' property from the 'useReduxContext(...)' because of its null value

I am currently using NextJs 13 along with redux toolkit. Whenever I run the npm run build command, I encounter this error: "Cannot destructure property 'store' of 'useReduxContext(...)' as it is null." I suspect that the issue lies wi ...

Setting values for TypeScript objects

I am currently working on developing a custom visual using Power BI. Within my interfaces file, I have a TypeScript interface called BiHiSankey. declare module 'd3' { interface BiHiSankey { nodeSpacing: () => number; ...

Error with React, key must be unique. What's the issue?

What is causing the issue with unique keys? To resolve the problem, ensure that each list item has a unique key. For example, if we have x_values = {'male':[1,2,3], 'female':[2,3,4]} the keys should be : 'mean-male', ' ...

NgControl was not found in the NodeInjector provider. How can I resolve this error?

https://i.sstatic.net/z4h8J.png I am encountering a problem that I have been unable to resolve despite extensive searching. Could you please provide suggestions on how to fix this issue? I have already included the following line in the application modu ...

A guide to sending epoch time data to a backend API using the owl-date-module in Angular

I have integrated the owl-date-time module into my application to collect date-time parameters in two separate fields. However, I am encountering an issue where the value is being returned in a list format with an extra null value in the payload. Additiona ...