This question has been asked already for different languages, mostly Python but I need the same for my Typescript classes.
Suppose I have this 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;
}
}
How can I now decorate my class
@TimeMethods()
class MyClass() { .... }
Sucht that I can time the exeuction of all functions. Such that the functions printX
and add(a,b)
are logged with their execution time, but the variables and getters _foo
and get foo
respectively are not.
I already wrote 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;
};
}
And I would like this to be automatically applied to each function in a class if the class is decorated as such.
Here is the final version of the mentioned interceptor. The summary of it: acquire all properties, intercept functions, skip constructor, and apply 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 real 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);
}
}
}
See it in action
Edit
As @CaTS mentioned using an async interceptor breaks the original sync function as it starts to return Promise
by default. If your environment uses non-native promises you should see the SO answer mentioned in the comments.