Search code examples
typescriptdecorator

Automatically call decorated functions


Currently, my code looks like this:

export class MyClass {
    public constructor() {
        this.doStuff1AtStartup();
        this.doStuff2AtStartup();
    }

    private doStuff1AtStartup(): void {
        // Do something here
    }

    private doStuff2AtStartup(): void {
        // Do something here
    }
}

However, this becomes very tedious If I need to add a few more functions and call them inside the constructor. As a result, I've tried to create decorators for my functions, remove the calls from the constructor and instead call the function inside my decorator function.

export class MyClass {
    @DecoratorWhichCallsMyClassAtStartup()
    private doStuffAtStartup() {
        // Do stuff here
    }
}

I can't figure out, though, how to keep the context (using this.something always returns undefined) - I figure that's because I'm now calling the function from my decorator function and thus can't get the reference to the class instance from there. I've also tried to call the original function by using bind or apply, but couldn't get it to work.

Can someone please suggest an implementation for this kind of thing?


Solution

  • With help from the documentation, I've come up with this:

    function AutoCall(methods: string[]) {
        return function <T extends { new (...args: any[]): any }>(constructor: T) {
            return class extends constructor {
                constructor(...args: any[]) {
                    super(...args);
    
                    for (const method of methods) {
                        super[method]();
                    }
                }
            }
        };
    }
    

    Pay attention to this line:

    If the class decorator returns a value, it will replace the class declaration with the provided constructor function.

    This lets us do what we want! We can replace the constructor with one that calls the methods automatically for us.

    And that's exactly what this decorator should do. Unfortunately I couldn't find a way to make it take keys of the class it is called on, so I had to use string[] here as the type for methods.

    Ideally it should also check if super[method] is an actual function, too.

    Here's an example usage as well:

    @AutoCall(["method"])
    class MyClass {
        private method() {
            console.log("i was called");
        }
    }
    
    new MyClass(); // logs "i was called"
    

    Playground