Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: Exception when combining a decorated class, a decorated method, and a static field #16373

Open
1 task
SirPepe opened this issue Mar 21, 2024 · 6 comments

Comments

@SirPepe
Copy link

SirPepe commented Mar 21, 2024

馃捇

  • Would you like to work on a fix?

How are you using Babel?

@rollup/plugin-babel

Input code

function decorate() {
  return function (target, context) {}
}

@decorate() // this does not have to be the same decorator as on the method
class Test {
  static something = 42;
  @decorate() // this does not have to be the same decorator as on the class
  method() {}
}

Configuration file name

babel.config.mjs

Configuration

/* eslint-env node */

const config = {
  presets: [["@babel/preset-env", {}], "@babel/preset-typescript"],
};

if (process.env.NODE_ENV === "test") {
  config.plugins = [
    [
      "@babel/plugin-proposal-decorators",
      {
        version: "2023-11",
      },
    ],
  ];
}

export default config;

Current and expected behavior

Current behavior: The code above results in TypeError: _initClass is not a function. This happens in my current setup when I combine a decorated class, a decorated method, and a static field. Removal of any of the aforementioned elements prevents the error from occurring.

Expected behavior: no exceptions.

Environment

System:
OS: Linux 6.5 Ubuntu 22.04.4 LTS 22.04.4 LTS (Jammy Jellyfish)
Binaries:
Node: 20.11.0 - ~/.volta/tools/image/node/20.11.0/bin/node
npm: 10.2.4 - ~/.volta/tools/image/node/20.11.0/bin/npm
npmPackages:
@babel/cli: ^7.22.5 => 7.24.1
@babel/core: ^7.22.1 => 7.24.3
@babel/plugin-proposal-decorators: ^7.22.3 => 7.24.1
@babel/preset-env: ^7.22.4 => 7.24.3
@babel/preset-typescript: ^7.21.5 => 7.24.1
eslint: ^8.41.0 => 8.57.0

Possible solution

No response

Additional context

No response

@babel-bot
Copy link
Collaborator

Hey @SirPepe! We really appreciate you taking the time to report an issue. The collaborators on this project attempt to help as many people as possible, but we're a limited number of volunteers, so it's possible this won't be addressed swiftly.

If you need any help, or just have general Babel or JavaScript questions, we have a vibrant Slack community that typically always has someone willing to help. You can sign-up here for an invite.

@SirPepe
Copy link
Author

SirPepe commented Mar 21, 2024

I stripped almost everything from my project: https://github.com/SirPepe/babel-decorators-repro

Running npm run build with the static field included results the following code:

function _identity(x) {
  return x;
}

let _initClass;
let _Test;
new class extends _identity {
  something = 42;
  constructor() {
    super(_Test), _initClass();
  }
}();

export { _Test as Test };

_initClass() is obviously not going to work. If I remove the static field from index.ts, _initClass() is there, alongside all the other helper functions that I would expect.

@JLHwung
Copy link
Contributor

JLHwung commented Mar 21, 2024

As a workaround, please set the typescript preset option allowDeclareFields: true. It will be defaulted to true in Babel 8 anyway.

This is essentially a plugin ordering issue: the typescript transform currently runs after the decorator transform, so the applyDecs call inserted in a temporary property is removed by the typescript transform, because historically ts doesn't use [[define]] for fields.

@SirPepe
Copy link
Author

SirPepe commented Mar 25, 2024

Thank you, this works for this particular problem! But allowDeclareFields has some confusing side effects in my code. I've reduced the issue to the following:

const key = Symbol();

function decorateClass(target: any, _: any): any {
  return class extends target {
    [key]!: any;
  };
}

function decorateMethod(_: any, context: any) {
  context.addInitializer(function (this: any) {
    this[key] ??= 1;
  });
}

@decorateClass
export class Test {
  @decorateMethod
  something() {}
}

console.log((new Test() as any)[key]);

The following happens when this runs:

  • with allowDeclareFields: true this logs undefined
  • with allowDeclareFields: false this logs 1
  • with allowDeclareFields: true and the line [key]!: any removed this logs 1

The idea behind my code is to have a set of decorators, where any of the decorator's initializers may lazily set up something on the class instance if and when that setup is needed.

I guess the next workaround would be to remove the line [key]!: any, which currently has no real effect at either runtime or compile time (because TS does not understand the effect of mixin classes in decorator). It only exists (with a proper type in place of any) to essentially document what the decorators are going to do - the lazy setup mentioned above. But in any case, I don't believe allowDeclareFields should have any effect on this code, as there's no declare anywhere in there?

@JLHwung
Copy link
Contributor

JLHwung commented Mar 25, 2024

In retrospect we should have chosen a better name for allowDeclareFields. The allowDeclareFields not only controls the parsing syntax declare field, but also mirrors the tsc compiler option useDefineForClassFields, which determines whether it should emit the ES class fields. When the compiler target is ES2022, tsc will automatically enable uDFCF, therefore we suggest enabling this option unless you want the legacy tsc behaviour.

So when allowDeclareFields is true, [key]!: any; will be transformed to key;, otherwise the field is removed. Therefore observation 2 and 3 are the same result.

with allowDeclareFields: true this logs undefined

The property defined in the derived class shadows the same property defined on the base class by the initializer of the something(), the [key] of the derived class is not manipulated, so undefined.

with allowDeclareFields: false this logs 1

The property is not defined in the derived class, so the (new Test() as any)[key] resolves to the base property with value 1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants