Home > OS >  Typescript decorators: why do some decorators require brackets and others dont?
Typescript decorators: why do some decorators require brackets and others dont?

Time:12-23

I am trying to understand typescript decorators.

In this example why does decorator1() require brackets when applied to a class method but decorator2 doesn't? My knowledge of TS decorators is not yet sufficient to make the distinction between the two types of decorator so any advice is appreciated.

https://codesandbox.io/s/typescript-decorator-forked-v3u6q?file=/src/index.ts

function decorate1() {
  console.log("decorate1(): factory evaluated");
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    console.log("decorate1(): called");
  };
}

function decorate2(target, key, descriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    // Call the original method
    console.log("calling decorate2 function");
    const result = original.apply(this, args);
    console.log("decorate2 returned", result);
    return result;
  };
  return descriptor;
}

class ExampleClass {
  @decorate1()
  @decorate2
  method() {
    return "something";
  }
}

const example = new ExampleClass();
example.method();

CodePudding user response:

At the outset it's important to remember that decorators are an experimental feature in TypeScript, and that the corresponding JavaScript proposed feature has changed considerably and is in Stage 2 of the TC39 proposal process. As such, they are probably best avoided if you haven't started relying on them yet. If and when the decorators proposal reaches Stage 3, TypeScript will modify the TS feature to conform to it, which could be a breaking change.


Summary: decorators are functions that take certain arguments and return certain values; you can decorate with any expression as long as it acts as a decorator. Parentheses are not part of the decorator declaration syntax; they are part of the expression. If you write @foo then you are using foo as a decorator. If you write @bar() then you are using bar(), not bar, as a decorator. It's exactly like const baz = bar() and then decorating with @baz.


Anyway, you are talking about method decorators. A method decorator is a function that accepts three arguments: the class prototype or constructor (depending on the static-ness of the method being decorated); the method name, and the property descriptor for the method. And it either returns nothing, or a new property descriptor.

So here is a method decorator:

const decorator = (
    target: any, key: PropertyKey, descriptor: PropertyDescriptor
) => {
    console.log("decorator", key);
}

You decorate a class method by putting a decorator declaration just before the method declaration. A decorator declaration looks like @ followed by a decorator expression. This can be any expression as long as it can act as a decorator; in this case, a function that conforms to the rule above:

class Foo {
    @decorator
    method1() { } //"decorator",  "method1" 
} 

You're decorating with the expression decorator. Note that there are no parentheses after decorator. If you wrote decorator() you would be calling decorator (with no arguments, which is wrong anyway) and since decorator() evaluates to undefined (it doesn't return a defined value), you'd be decorating with undefined, and undefined is not a decorator.


If the decorator needs more information in order to run, conceptually you'd like it to take more arguments than the required three, but that's not allowed. What is allowed is to make a function that takes the extra information and returns a decorator. Here's a function that returns a decorator:

const fnReturningDecorator = (name: string) => (
    target: any, key: PropertyKey, descriptor: PropertyDescriptor
) => {
    console.log("fnReturningDecorator", name, key);
}

And here's how you use it to decorate a method:

class Foo {    
    @fnReturningDecorator("hello") 
    method2() { } // "fnReturningDecorator",  "hello",  "method2" 
}

You're decorating with the expression fnReturningDecorator("hello"). Note that you have to call fnReturningDecorator with its string argument and then decorate with the returned decorator. If you wrote fnReturningDecorator with no arguments, you'd be decorating with a function which takes a single string argument and returns a function, and that's not a decorator. Again, a function that returns a decorator is not itself a decorator.

This is equivalent to:

const hello = fnReturningDecorator("hello");

class Foo {
    @hello
    method2() { } // "fnReturningDecorator",  "hello",  "method2" 
}   

So there's nothing special about the parenthesis. The parentheses are just function calls so that you get a decorator out; they are not part of the decorator syntax.


Again, any expression which evaluates to a proper decorator will work. As a final example, here's an object with a property that is a decorator:

const objHoldingDecorator = {
    decorator: (
        target: any, key: PropertyKey, descriptor: PropertyDescriptor
    ) => {
        console.log("objHoldingDecorator", key);
    }
}

And now when we want to decorate with the decorator, we do it this way:

class Foo {
    @objHoldingDecorator.decorator
    method3() { } // "objHoldingDecorator",  "method3" 
}

Again, no parentheses, but this time we have a dot. The decorator is objHoldingDecorator.decorator; if you tried decorating with just objHoldingDecorator, you'd be decorating with a non-callable object, and that's not a decorator.


Playground link to code

  • Related