Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript decorators not working with arrow functions

I have a typescript decorator factory which console logs total time taken to execute a function, actual function execution results and parameters passed to the decorator as well.

e.g.

export function performaceLog(...args: any[]) {
return (target: Object, key: string, descriptor: TypedPropertyDescriptor<any>) => {
    var msg = '';
    if (args.length != 0) {
        msg = args.map(arg => arg).join(' ');
    }

    if (descriptor === undefined) {
        descriptor = Object.getOwnPropertyDescriptor(target, key);
    }

    if (typeof descriptor.value !== 'function') {
        throw new SyntaxError('Only functions can be used with log decorators');
    }

    var originalMethod = descriptor.value.bind(target);

    descriptor.value = function() {
        var funcArgs: any = [];
        for (var i = 0; i < arguments.length; i++) {
            funcArgs[i - 0] = arguments[i];
        }
        var startTime = performance.now();
        console.log(`Begin function ${key} with params (${funcArgs}) : ${msg}`);
        var result = originalMethod.apply(this, funcArgs);
        var endTime = performance.now();
        console.log(`End function ${key}. Execution time: ${endTime - startTime} milliseconds. Return result : ${result}`);
        return result;
    };
    return descriptor;
};
}

I am using the above decorator with a function which is present in a class: (considering my class has other methods as well like ctor, get, set and utils).

class LoggerService {
    @performaceLog('validate', 'message')
    handleMessage(message) {
        console.log(message);
        return true;
    };
}

Function call looks like this:

handleMessage('decoratorValidation');

This gives me perfect output as:

Begin function handleMessage with params (decoratorValidation) : validate message     
decoratorValidation
End function handleMessage. Execution time: 0.3000000142492354 milliseconds. 
Return result : true

But when I change the function handleMessage to support arrow format (ES6), it throws me an error:

@performaceLog('validate', 'message')
handleMessage = message => {
    console.log(message);
    return true;
};

Error Message:

Unable to resolve signature of property decorator when called as an expression.

I have all the parameters set correctly in my tsconfig.json. I have my entire project supporting "ES6" target and I want the decorator to support arrow functions.

like image 754
Shreyas Patil Avatar asked Apr 19 '18 18:04

Shreyas Patil


People also ask

Does TypeScript support arrow function?

ES6 version of TypeScript provides an arrow function which is the shorthand syntax for defining the anonymous function, i.e., for function expressions. It omits the function keyword. We can call it fat arrow (because -> is a thin arrow and => is a "fat" arrow).

Are decorators still experimental in TypeScript?

Decorators provide a way to annotate or modify a class or class member in TypeScript. However, Decorators are still an experimental feature of the language. To learn more about Decorators in TypeScript, visit the Decorators proposal page.

Where can TypeScript decorators be applied to?

A Decorator is a special kind of declaration that can be applied to classes, methods, accessor, property, or parameter.

When you should not use arrow functions?

An arrow function doesn't have its own this value and the arguments object. Therefore, you should not use it as an event handler, a method of an object literal, a prototype method, or when you have a function that uses the arguments object.


1 Answers

performaceLog is supposed to work with prototype methods only because it relies on descriptor, which should be optional.

There is no descriptor in case of handleMessage = message => ... class field because it doesn't exist on class prototype. Class fields are just assigned to this in constructor.

This line won't work for same reason:

descriptor = Object.getOwnPropertyDescriptor(target, key);

In order to patch arrow method in decorator, a trap should be set on class prototype. Here is an example of universal decorator that can be used with both prototype and instance methods; it uses get/set to catch proper this context and cache decorated function to patchFn variable. It returns a descriptor regardless of descriptor parameter:

function universalMethodDecorator(target: any, prop: string, descriptor?: TypedPropertyDescriptor<any>): any {
    let fn;
    let patchedFn;

    if (descriptor) {
        fn = descriptor.value;
    }

    return {
        configurable: true,
        enumerable: false,
        get() {
            if (!patchedFn) {
                patchedFn = (...args) => fn.call(this, ...args);
            }
            return patchedFn; 
        },
        set(newFn) {
            patchedFn = undefined;
            fn = newFn;
        }
    };

}

This applies only to TypeScript decorators. Babel legacy decorators may behave differently.

As explained in this answer, prototype methods can be preferred to instance methods for several reasons. One of the reasons is that they can be seamlessly decorated because decorators are applied to class prototype. The only real benefit of arrow method is that it is naturally bound to class instance, but since the decorator is already in use, prototype method can be bound in decorator if needed (this is what universalMethodDecorator basically does; it is noop for arrow methods).

like image 121
Estus Flask Avatar answered Oct 23 '22 03:10

Estus Flask