I have a class decorator and would like to use it to apply another decorator to all methods within the class, but I am not quite sure how to apply a decorator programmatically without the # syntax:
#LogAllMethods
class User {
greet() {
console.log('Hello')
}
}
function LogAllMethods(target) {
for (const key in Object.keys(target.prototype)) {
// apply LogMethod decorator
}
}
function LogMethod(target, key, descriptor) {
let method = descriptor.value
decriptor.value = function(...args) {
console.log(args)
method.apply(this, ...args)
}
}
You basically just have to call the decorator function with the target, the key (method name) and the defined descriptor:
function LogAllMethods<T>(target: new (...params: any[]) => T) {
for (const key of Object.getOwnPropertyNames(target.prototype)) {
let descriptor = Object.getOwnPropertyDescriptor(target.prototype, key);
descriptor = LogMethod(target.prototype, key, descriptor);
if (descriptor) {
Object.defineProperty(target.prototype, key, descriptor);
}
}
}
function LogMethod(target: any, key: symbol | string, descriptor: TypedPropertyDescriptor<any> = undefined) {
if (descriptor) {
let method = descriptor.value;
if (method instanceof Function) {
descriptor.value = function (...args: any[]) {
console.log("Log", args)
method.apply(this, ...args);
}
}
return descriptor;
} else {
// descriptor is null for target es5 if the decorator is applied directly to the merhod
let method = target[key];
if (method instanceof Function) {
target[key] = function (...args: any[]) {
console.log("Log", args)
method.apply(this, ...args);
}
}
}
}
Related
I want to create a wrapper function for an existing function in TypeScript.
The wrapper function could start some other process and clean it up after finishing the main ("callback") function passed to the wrapper.
This can be done using approaches like shown here. However, these solutions do not allow me to specify additional options that can be passed to the wrapper itself.
How would I go about doing that?
My starting point was:
export const wrap = async <T>(
callback: () => T | Promise<T>,
options?: { foo?: string | undefined },
): Promise<T> => {
let ret;
// begin
if (options.foo) {
// do something
}
try {
ret = await callback();
} catch (e) {
throw e;
} finally {
// cleanup
}
return ret;
};
This would not let me add arguments to callback(). I can use ...args, but how would I specify both ...args and options?
I suggest you to write your function as a factory (function that returns another function):
function wrap<T extends (...args: any[]) => any>(callback: T, options?: { foo?: string | undefined }): (...args: Parameters<T>) => ReturnType<T> extends Promise<infer U> ? Promise<U> : Promise<ReturnType<T>>
function wrap(callback: (...args: any[]) => any, options?: { foo?: string | undefined }): (...args: any[]) => Promise<any> {
return async function (...args: any[]) {
let ret;
// begin
if (options && options.foo) {
// do something
}
try {
ret = await callback(...args);
} catch (e) {
throw e;
} finally {
// cleanup
}
return ret;
}
};
async function asyncExtractFirstParameter(str: string, num: number, bool: boolean) {
return str;
}
function extractFirstParameter(str: string, num: number, bool: boolean) {
return str;
}
const wrappedAsyncExtractFirstParameter = wrap(asyncExtractFirstParameter);
const wrappedExtractFirstParameter = wrap(extractFirstParameter);
wrappedAsyncExtractFirstParameter('test', 23, true).then(console.log);
wrappedExtractFirstParameter('test', 23, true).then(console.log);
That allows you to create another function with the same signature as the callback, and possibly defer its execution.
TypeScript playground
The solution consists of making the functon generic (F) and inferring the parameters and return types of that function via ReturnType and Parameters. Any additional options can be specified in advance of the destructured remaining arguments which are passed to the callback function:
export const wrap = async <F extends (...args: any[]) => ReturnType<F> | Promise<ReturnType<F>>>(
callback: F,
options: { foo?: string | undefined } = {},
...args: Parameters<F>
): Promise<ReturnType<F>> => {
let ret: ReturnType<F> | undefined;
// begin
if (options.foo) {
// do something
}
try {
ret = await callback(...args);
} catch (e) {
throw e;
} finally {
// cleanup
}
return ret;
};
The wrapper can then be called like this:
const someFunction = (arg1: string, arg2: boolean): void => {
// do something
}
wrap(someFunction, { foo: "yes" }, arg1, arg2)`
All return types and argument types are inferred automatically.
I am trying out decorators, I have written a decorator that basically returns a new function that does some `console.log.
This is what my decorator looks like:
function test(target, name, descriptor) {
const original = descriptor.value;
console.log("bbau");
if (typeof original === 'function') {
descriptor.value = function (...args) {
console.log(`Arguments: ${args}`);
try {
console.log("executing");
const result = original.apply(this, args);
console.log("done");
console.log(`Result: ${result}`);
return result;
} catch (e) {
console.log(`Error: ${e}`);
throw e;
}
}
}
return descriptor;
}
And this is how I am using it:
class TestController extends BaseController<//..> {
// ...
#test
testIt(req: Request, res: Response) : Response {
this.sendResponse();
}
sendResponse(options: ISendResponseOptions, res: Response) : Response {
// return response
}
}
``
However, when executed an error is raised: Error: TypeError: Cannot read property 'sendResponse' of undefined.
Any thoughts about what it could be? Thanks!
You should generally use an arrow function when you want to capture this from the context you declared the function in (or when this does not matter). In this case you really want this to be the object the function was called on so you should use a regular function :
const test = (target, name, descriptor) => {
const original = descriptor.value;
if (typeof original === 'function') {
descriptor.value = function (...args) {
console.log(`Arguments: ${args}`);
try {
console.log("executing");
const result = original.apply(this, args);
console.log("done");
console.log(`Result: ${result}`);
return result;
} catch (e) {
console.log(`Error: ${e}`);
throw e;
}
}
}
return descriptor;
}
You can test it out in the playground
If you use this function as a parameter to another function you should also call bind to set this for the function (otherwise the caller will determine the value of this):
router.route("/").post(testController.testIt.bind(testController))
Imagine you have to change method arguments at runtime, using a decorator. A trivial example to make it simple: all arguments being set to "Hello World":
export const SillyArguments = (): MethodDecorator => {
return (
target: Object,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) => {
const originalMethod = descriptor.value;
descriptor.value = (...args: any[]) => {
Object.keys(args).forEach(i => {
args[i] = 'Hello World';
});
return originalMethod.apply(null, args);
};
return descriptor;
}
};
Example usage:
class TestClass {
private qux = 'qux';
#SillyArguments()
foo(val: any) {
console.log(val);
console.log(this.qux);
this.bar();
}
bar() {
console.log('bar');
}
}
const test = new TestClass();
test.foo('Ciao mondo'); // prints "Hello World"
TypeError: Cannot read property 'qux' of null
The problem here is apply(null, args), which changes the context of this. This makes impossible to call the instance variable named qux, from inside foo().
Another possibility is to change the call to originalMethod.apply(target, args), but this time qux is undefined, while bar() can be invoked.
Is there any possibility to invoke the originalMethod with the context of this correctly set to the instance?
Use a function function instead of an arrow function so that you receive the original this context and can pass it along:
export const SillyArguments = (): MethodDecorator => {
return (
target: Object,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) => {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
Object.keys(args).forEach(i => {
args[i] = 'Hello World';
});
return originalMethod.apply(this, args);
};
return descriptor;
}
};
I'm trying to build an AOP logger for my classes... I'm having an issue where when i reflect back to the targeted function, the function loses access to this
so my AOP kinda looks like this
AOP.js
class AOP {
constructor() {
}
static ClassHandler(obj) {
const InstanceHandler = {
get(target, prop, receiver) {
console.log(target.constructor.name);
const origMethod = target[prop];
return function (...args) {
// let result = Reflect.apply(origMethod, this, args)
let result = Reflect.get(target, prop, receiver)
result = Reflect.apply(result, this, args);
console.log(prop + JSON.stringify(args)
+ ' -> ' + JSON.stringify(result));
return result;
};
},
apply(target, thisArg, argumentsList) {
console.log('actually applied');
}
}
const handler = {
construct(target, args) {
console.log(`${target.name} instantiated`);
console.log(args);
const instance = Reflect.construct(...arguments);
return new Proxy(instance, InstanceHandler);
}
}
return new Proxy(obj, handler);
}
}
module.exports = AOP;
A singleton
OtherClass.js
class OtherClass {
constructor() {
this._blah = 'this is a shoutout';
}
shoutOut() {
console.log(this._blah);
}
}
module.exports = new OtherClass();
and a class which requires the singleton
CalculatorDI.js
class Calculator {
constructor(otherClass) {
this.otherClass = otherClass;
}
add(a, b) {
this.otherClass.shoutOut();
return a+b;
}
minus(a, b) {
return a-b;
}
}
module.exports = Calculator;
bringing it all together like this:
const AOP = require('./src/aspects/AOP');
const Calculator = AOP.ClassHandler(require('./src/CalculatorDI'));
const otherClass = require('./src/OtherClass');
const calculator = new Calculator(otherClass);
calculator.add(1,1);
When running this, I get the error:
TypeError: this.otherClass.shoutOut is not a function
Your problem is that your proxy always returns a function, for any property that is accessed, including this.otherClass. You will need to use
const instanceHandler = {
get(target, prop, receiver) {
console.log(target.constructor.name);
const orig = Reflect.get(target, prop, receiver);
if (typeof orig == "function") {
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
return function (...args) {
const result = orig.apply(this, args);
console.log(prop + JSON.stringify(args) + ' -> ' + JSON.stringify(result));
return result;
};
} else {
return orig;
}
}
};
Also notice that you don't need an apply trap in the instanceHandler, as none of your instances is a function.
I have a class that I'd like to apply a proxy to, observing method calls and constructor calls:
Calculator.js
class Calc {
constructor(){}
add(a, b) {
return a+b;
}
minus(a, b) {
return a-b;
}
}
module.exports = Calc;
index.js
const Calculator = require('./src/Calculator');
const CalculatorLogger = {
construct: function(target, args, newTarget) {
console.log('Object instantiated');
return new target(...args);
},
apply: function(target, thisArg, argumentsList) {
console.log('Method called');
}
}
const LoggedCalculator = new Proxy(Calculator, CalculatorLogger);
const calculator = new LoggedCalculator();
console.log(calculator.add(1,2));
When this is called, I would expect for the output to be:
Object instantiated
Method called
however, the apply is not being called, I assume that this is because I am attaching the Proxy to the Calculator class, but not the instantiated object, and so doesn't know about the apply trap.
How can i build an all encompassing Proxy to "observe" on method calls and constructor calls.
I assume that this is because I am attaching the Proxy to the Calculator class, but not the instantiated object, and so doesn't know about the apply trap.
You are totally right, proxies act upon objects, so it won't call apply unless a function property of the Calculator class is called, as follows:
class Calculator {
constructor() {
this.x = 1;
}
instanceFunction() {
console.log('Instance function called');
}
static staticFun() {
console.log('Static Function called');
}
}
const calcHandler = {
construct(target, args) {
console.log('Calculator constructor called');
return new target(...args);
},
apply: function(target, thisArg, argumentsList) {
console.log('Function called');
return target(...argumentsList);
}
};
Calculator = new Proxy(Calculator, calcHandler);
Calculator.staticFun();
const obj = new Calculator();
obj.instanceFunction();
With that clear, what you could do to wrap an instance of Calculator with a proxy could be:
Have the class proxy to proxify instances on construct:
const CalculatorInstanceHandler = {
apply(target, thisArg, args) {
console.log('Function called');
return target(...args);
}
}
const CalculatorClassHandler = {
construct(target, args) {
const instance = new target(...args);
return new Proxy(instance, CalculatorInstanceHandler);
}
}
Have a factory function in the Calculator class in order to create proxified instances:
const CalculatorInstanceHandler = {
apply(target, thisArg, args) {
return target(...args);
}
};
class Calculator {
static getNewCalculator() {
const instance = new Calculator();
return new Proxy(instance, CalculatorInstanceHandler);
}
}
Instead of using handler.apply() on the class, modify what handler.construct() returns, adding a Proxy to that instead.
class originalClass {
constructor() {
this.c = 1;
}
add(a, b) {
return a + b + this.c;
}
}
const proxiedClass = new Proxy(originalClass, {
construct(target, args) {
console.log("constructor of originalClass called.");
return new Proxy(new target(...args), {
get(target, prop, receiver) {
console.log(prop + " accessed on an instance of originalClass");
const val = target[prop];
if (typeof target[prop] === "function") {
console.log(prop + " was a function");
return function(...args) {
console.log(prop + "() called");
return val.apply(this, args);
};
} else {
return val;
}
}
});
}
});
const proxiedInstance = new proxiedClass();
console.log(proxiedInstance.add(1, 2));
There's 2 proxies in play here:
A proxy to observe constructor calls, and wrap any instances created by that constructor with...
...a proxy to observe property accesses, and log when those properties are functions. It will also wrap any functions, so it can observe calls to that function.