Chris
Chris

Reputation: 14260

JS TS apply decorator to all methods / enumerate class methods

I would like to apply a decorator function to all methods within a class so I can replace:

class User {
    @log
    delete() {}

    @log
    create() {}

    @log
    update() {}
}

with

@log
class User {
    delete() {}
    create() {}
    update() {}
}

Upvotes: 29

Views: 8486

Answers (3)

Julius Žaromskis
Julius Žaromskis

Reputation: 2265

If you don't feel like pulling additional dependencies, here's a simplified version of @Papooch's implementation

function DecorateAll(decorator: MethodDecorator) {
    return (target: any) => {
        const descriptors = Object.getOwnPropertyDescriptors(target.prototype);
        for (const [propName, descriptor] of Object.entries(descriptors)) {
            const isMethod =
                typeof descriptor.value == "function" &&
                propName != "constructor";
            if (!isMethod) {
                continue;
            }
            decorator(target, propName, descriptor);
            Object.defineProperty(target.prototype, propName, descriptor);
        }
    };
}

function Throttle(
    target: any,
    propertyKey: string | symbol,
    descriptor: PropertyDescriptor
) {
    const original = descriptor.value;
    descriptor.value = function () {
        console.log("throttle");
        return original.call(this);
    };
}

@DecorateAll(Throttle)
class SharedApiClient {
    async fetch1() {    }
    async fetch2() {    }
}

Upvotes: 3

Papooch
Papooch

Reputation: 1645

For whomever stumbles upon this in the future:

I took inspiration in David's answer and created my own version. I later made it into a npm package: https://www.npmjs.com/package/decorate-all

In OP's scenario, it would be used like this

@DecorateAll(log)
class User {
    delete() {}
    create() {}
    update() {}
}

Upvotes: 12

David Sherret
David Sherret

Reputation: 106900

Create a class decorator and enumerate the properties on the target's prototype.

For each property:

  1. Get the property descriptor.
  2. Ensure it's for a method.
  3. Wrap the descriptor value in a new function that logs the information about the method call.
  4. Redefine the modified property descriptor back to the property.

It's important to modify the property descriptor because you want to ensure your decorator will work well with other decorators that modify the property descriptor.

function log(target: Function) {
    for (const propertyName of Object.keys(target.prototype)) {
        const descriptor = Object.getOwnPropertyDescriptor(target.prototype, propertyName);
        const isMethod = descriptor.value instanceof Function;
        if (!isMethod)
            continue;

        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            console.log("The method args are: " + JSON.stringify(args));
            const result = originalMethod.apply(this, args);
            console.log("The return value is: " + result);
            return result;
        };

        Object.defineProperty(target.prototype, propertyName, descriptor);        
    }
}

Base Class Methods

If you want this to affect the base class methods as well, then you might want something along these lines:

function log(target: Function) {
    for (const propertyName in target.prototype) {
        const propertyValue = target.prototype[propertyName];
        const isMethod = propertyValue instanceof Function;
        if (!isMethod)
            continue;

        const descriptor = getMethodDescriptor(propertyName);
        const originalMethod = descriptor.value;
        descriptor.value = function (...args: any[]) {
            console.log("The method args are: " + JSON.stringify(args));
            const result = originalMethod.apply(this, args);
            console.log("The return value is: " + result);
            return result;
        };

        Object.defineProperty(target.prototype, propertyName, descriptor);        
    }

    function getMethodDescriptor(propertyName: string): TypedPropertyDescriptor<any> {
        if (target.prototype.hasOwnProperty(propertyName))
            return Object.getOwnPropertyDescriptor(target.prototype, propertyName);

        // create a new property descriptor for the base class' method 
        return {
            configurable: true,
            enumerable: true,
            writable: true,
            value: target.prototype[propertyName]
        };
    }
}

Upvotes: 34

Related Questions