javascriptjavascript-decorators

How to decorate a JavaScript class constructor when using non-legacy decorators?


I have the following class:

@log
class Example {
  constructor(name, age) {
    console.log("Example constructor", name, age);
    this.name = name;
    this.age = age;
  }
}

And this @log decorator when using legacy decorators:

function log(Class) {
  return class extends Class {
    constructor(...args) {
      console.log("log", args);
      super(...args);
    }
  };
}
// .babelrc
// ...
      "plugins": [
        ["@babel/plugin-proposal-decorators", { "legacy": true }],
        // ...
      ]
// ...

The above setup works well, and the following code:

const example = new Example("Graham", 34);

Outputs:

log (2) ["Graham", 34]
Example constructor Graham 34

Now, how can I achieve the same result when using non-legacy decorators, i.e.:

// .babelrc
// ...
      "plugins": [
        [
          "@babel/plugin-proposal-decorators",
          { "decoratorsBeforeExport": false }
        ],
        // ...
      ]
// ...

How can I implement @log so that it works in the same way as with legacy decorators?

function log(...args) {
  const [descriptor] = args;
  const { kind, elements } = descriptor;

  const newElements = elements.concat({
    kind: "method",
    placement: "prototype",
    key: "constructor",
    descriptor: {
      value: (...args) => {
        // I have tried this but it doesn't work...
        console.log("log", args);
      },
      configurable: true,
      writable: true,
      enumerable: true,
    },
  });

  return {
    kind,
    elements: newElements,
  };
}

I have tried the code above but it didn't work. The problem is that I do not have a reference to the target when using non-legacy decorators. Do you know if there is a way to achieve the same behaviour as with the legacy ones?

Thank you.


Solution

  • I have found the answer, thanks to loganfsmyth on the BabelJS Slack channel.

    You have to return an object with a property called finisher wrapping the class, e.g.:

    function log(...args) {
      const [descriptor] = args;
      const { kind, elements } = descriptor;
    
      return {
        kind,
        elements,
        finisher: (Class) => {
          return class extends Class {
            constructor(...args) {
              console.log("log", args);
              super(...args);
            }
          };
        }
      };
    }
    

    Then this code when using non-legacy decorators:

    @log
    class Example {
      constructor(name, age) {
        console.log("Example constructor", name, age);
        this.name = name;
        this.age = age;
      }
    }
    
    const example = new Example("Graham", 34);
    
    

    Outputs:

    log (2) ["Graham", 34]
    Example constructor Graham 34
    

    As with legacy decorators.

    It was difficult to find the answer because this feature of non-legacy decorators is not documented on @babel/plugin-proposal-decorators.