Search code examples
javascripttypescriptdecoratorecmascript-2016

How to avoid hard coded this? in Decorators


I have read "How to implement a typescript decorator?" and multiple sources but there is something that i have nor been able to do with decorators.

class FooBar {
    public foo(arg): void { 
        console.log(this);
        this.bar(arg);
    }
    private bar(arg) : void { 
        console.log(this, "bar", arg);
    }
}

If we invoke the function foo:

var foobar = new FooBar();
foobar.foo("test"); 

The object FooBar is logged in the console by console.log(this); in foo

The string "FooBar {foo: function, bar: function} bar test" is logged in the console by console.log(this, "bar", arg); in bar.

Now let's use a decorator:

function log(target: Function, key: string, value: any) {
    return {
        value: (...args: any[]) => {
            var a = args.map(a => JSON.stringify(a)).join();
            var result = value.value.apply(this, args); // How to avoid hard coded this?
            var r = JSON.stringify(result);
            console.log(`Call: ${key}(${a}) => ${r}`);
            return result;
        }
    };
}

We use the same function but decorated:

class FooBar {
    @log
    public foo(arg): void { 
        console.log(this);
        this.bar(arg);
    }
    @log
    private bar(arg) : void { 
        console.log(this, "bar", arg);
    }
}

And we invoke foo as we did before:

var foobarFoo = new FooBar();
foobarFooBar.foo("test");

The objectWindow is logged in the console by console.log(this); in foo

And bar is never invoked by foo because this.bar(arg); causes Uncaught TypeError: this.bar is not a function.

The problem is the hardcoded this inside the log decorator:

value.value.apply(this, args);

How can I conserve the original this value?


Solution

  • Don't use an arrow function. Use a function expression:

    function log(target: Object, key: string, value: any) {
        return {
            value: function(...args: any[]) {
                var a = args.map(a => JSON.stringify(a)).join();
                var result = value.value.apply(this, args);
                var r = JSON.stringify(result);
                console.log(`Call: ${key}(${a}) => ${r}`);
                return result;
            }
        };
    }
    

    That way it will use the function's this context instead of the value of this when log is called.


    By the way, I would recommend editing the descriptor/value parameter and return that instead of overwriting it by returning a new descriptor. That way you keep the properties currently in the descriptor and won't overwrite what another decorator might have done to the descriptor:

    function log(target: Object, key: string, descriptor: TypedPropertyDescriptor<any>) {
        var originalMethod = descriptor.value;
    
        descriptor.value = function(...args: any[]) {
            var a = args.map(a => JSON.stringify(a)).join();
            var result = originalMethod.apply(this, args);
            var r = JSON.stringify(result);
            console.log(`Call: ${key}(${a}) => ${r}`);
            return result;
        };
    
        return descriptor;
    }
    

    More details in this answer - See the "Bad vs Good" example under "Example - Without Arguments > Notes"