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!
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
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
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With