Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I access the target class instance in a Typescript method decorator?

I'm creating a WebSocket server in Typescript in which different application components should be able to register their own request handlers. There's a singleton WebsocketHandler that provides this behavior.

Without decorators, a class can register its request handlers like this:

class ListOfStuff {
  private list = [];

  constructor() {
    //Register listLengthRequest as a request handler
    WebsocketHandler.getInstance().registerRequestHandler('getListLength', () => this.listLengthRequest());
  }

  private listLengthRequest() : WebSocketResponse {
    return new WebSocketResponse(this.list.length);
  }
}

class WebsocketHandler {
  private constructor() {}

  private static instance = new WebsocketHandler();

  static getInstance() {
    return this.instance;
  }

  registerRequestHandler(requestName: string, handler: () => WebSocketResponse) {
    //Store this handler in a map for when a request is received later 
  }
}

class WebSocketResponse {
  constructor(content: any) {}
}

Which works fine. However, I'm trying to replace the registration calls in the constructor with method decorators. Ideally, ListOfStuff would then look like this:

class ListOfStuff {
  private list = [];

  @websocketRequest("getListLength")
  private listLengthRequest() : WebSocketResponse {
    return new WebSocketResponse(this.list.length);
  }
}

But, after creating a decorator factory for @websocketRequest, I can't figure out how to have listLengthRequest() execute in the correct context. I tried this factory function:

function websocketRequest(requestName: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    WebsocketHandler.getInstance().registerRequestHandler(requestName, descriptor.value);
  }
}

Which makes this equal to the map the function is being held in (inside WebsocketHandler).

Then I tried passing in target as the handler's context with this factory function:

function websocketRequest(requestName: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    WebsocketHandler.getInstance().registerRequestHandler(requestName, () => descriptor.value.call(target));
  }
}

But I then realised that target only refers to the prototype of ListOfStuff and not the actual class instance. So still not helpful.

Is there any way I can get the instance of ListOfStuff (so I can access this.list) in the decorator factory? Should I be structuring my decorator in some other way so that it's tied to the instance of the class rather than its prototype? Here's a Repl demonstrating the issue https://repl.it/@Chap/WonderfulLuckyDatalogs

This is the first time I've messed around with decorators, and I'm pretty new to Typescript too, so any guidance would be greatly appreciated. Thanks!

like image 611
Chap Callanan Avatar asked Dec 22 '22 18:12

Chap Callanan


2 Answers

It is not possible to access an instance in a Typescript method decorator. But it is possible to change the prototype and constructor using decorators. So the possible solution is to use two decorators: the first one that "marks" methods and the second one that changes the constructor adding the registration logic.

Below I'll try to illustrate the idea

const SubMethods = Symbol('SubMethods'); // just to be sure there won't be collisions

function WebsocketRequest(requestName: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    target[SubMethods] = target[SubMethods] || new Map();
    // Here we just add some information that class decorator will use
    target[SubMethods].set(propertyKey, requestName);
  };
}

function WebSocketListener<T extends { new(...args: any[]): {} }>(Base: T) {
  return class extends Base {
    constructor(...args: any[]) {
      super(...args);
      const subMethods = Base.prototype[SubMethods];
      if (subMethods) {
        subMethods.forEach((requestName: string, method: string) => {
          WebsocketHandler.getInstance()
            .registerRequestHandler(
              requestName,
              () => (this as any)[method]()
            );
        });
      }
    }
  };
}

Usage:

@WebsocketListener
class ListOfStuff {
  private list = [];

  @WebsocketRequest("getListLength")
  private listLengthRequest() : WebSocketResponse {
    return new WebSocketResponse(this.list.length);
  }
}

Updated repl

like image 135
Shlang Avatar answered Apr 25 '23 23:04

Shlang


The following code snippet shows how we could access the target class instance from within a decorator for methods:

function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalValue = descriptor.value;

  descriptor.value = function(...args: any[]) {
    // "this" here will refer to the class instance
    console.log(this.constructor.name);

    return originalValue.apply(this, args);
  }
};

Usage:

class Foo {

  @log
  bar() {
    // do something
  }
}

like image 27
Abhishek Borar Avatar answered Apr 25 '23 22:04

Abhishek Borar