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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

typescript: literal constructor references of a decorated class reference un-decorated class's constructor, when used inside decorated class #8900

Open
benasher44 opened this issue Apr 27, 2024 · 3 comments
Assignees
Labels
Milestone

Comments

@benasher44
Copy link

benasher44 commented Apr 27, 2024

Describe the bug

Literal references to the constructor inside the class's implementation reference the un-decorated class's constructor. This results in functions, like a copy function, unexpectedly returning instances of the class without decoration.

Input code

function markedClass<T extends Constructor<any>>(
  constructor: T,
  markerName: string
): T & Constructor<any> {
  const result = {
    [constructor.name]: class extends constructor {
      constructor(...args: any[]) {
        super(...args);
        Object.assign(this, { __markerName: markerName });
      }
    },
  };
  return result[constructor.name];
}

function MarkClass(markerName: string) {
  return <T extends Constructor<object>>(target: T): T => {
    return markedClass(target, markerName);
  };
}

@MarkClass("example")
export class Example {
  public copy() {
    return new Example();
  }
}

Config

{
  "jsc": {
    "parser": {
      "syntax": "typescript",
      "tsx": false,
      "decorators": true
    },
    "transform": {
      "legacyDecorator": true,
      "decoratorMetadata": true,
      "useDefineForClassFields": false
    },
    "target": "es2015",
    "minify": {
      "mangle": false,
      "compress": false
    },
    "loose": false
  },
  "isModule": true,
  "module": {
    "type": "commonjs"
  },
  "minify": false
}

Playground link (or link to the minimal reproduction)

https://play.swc.rs/?version=1.5.2&code=H4sIAAAAAAAAA3VRzWqDQBC%2BC77DkENZQXwAm0gh9Nj2klsIYbOZGFtdZXelhuC7d7Kurq30tsx%2B8%2F3NpZXCFLWEiqsvPG9LrvV6B9gZlGcN21pqo1pharXm8pZlLAwAhJ%2BmsIsfI7uu3nmFKdBfIfMwiOgTnhYccJ84QKFuSwObYQawn1EnktgOKYiHp8nRDDAu%2FTLEkiThKtcpkNb%2BEHkQgG4bnADRs%2F%2F4OH2iMAnpFLlk5lroGO5wPM5T%2BTf0frcfHr0tobdjhaZV0kVbBiIMLYXBZWz%2BjYht72xZorPvKP85TG3d020M5UJDrdvmN9mY3a3PTuyg8SzVkKkf7b14WyvseNWUuIrCALumVsbd5HWYDzJNeyoLQadobiz6oyzxewQzp2NlfgCGAagKfgIAAA%3D%3D&config=H4sIAAAAAAAAA21QQY6DMAy87yuQzz1sV%2Bql16Le%2BggrGBSUxMg2UlHVvzdQgtjt3uKZzHjGj6%2Bqgl4dnKtHfuZhQFGSbc6ITsnwnhGwaSB14geDQ2FNZ6rFoLRhDTkWNBbNlMlIC%2FF882CCSVuWuF8SqEM31UW46j4db2TYoOHfD6NSTa1PdGW5BFS9egqNlmi%2FA6B0ZHMh0p%2Fv42ktA9En3077VBFTF%2Bijn%2BM4COn%2F7oFZaccsOHi9cTMuXiU3xIKsp5%2FPO6fK9pFTr7Cpt2Rv0%2BcLSEYzDLcBAAA%3D

SWC Info output

Operating System:
    Platform: darwin
    Arch: arm64
    Machine Type: arm64
    Version: Darwin Kernel Version 23.4.0: Fri Mar 15 00:10:42 PDT 2024; root:xnu-10063.101.17~1/RELEASE_ARM64_T6000
    CPU: (10 cores)
        Models: Apple M1 Max

Binaries:
    Node: 20.11.1
    npm: 10.2.4
    Yarn: 4.0.1
    pnpm: N/A

Relevant Packages:
    @swc/core: 1.5.0
    @swc/helpers: N/A
    @swc/types: 0.1.6
    typescript: 5.4.5

SWC Config:
    output: N/A
    .swcrc path: N/A

Next.js info:
    output: N/A

Expected behavior

With the example setup, calling (new Example()).copy() should return a new instance of Example that is also decorated.

Actual behavior

Instead you can an un-decorated Example instance.

Version

1.5.0

Additional context

The workaround is to ensure that you do new this.constructor(), when attempting to create a new version of a class from inside that same class. Upon inspecting tsc emit, this is exactly how typescript handles it: transforming such new Example() (where Example is the constructor of the class owning the function where the constructor is being called) code to be new this.constructor() instead.

@benasher44 benasher44 changed the title typescript: literal constructor references of a decorated class reference un-decorated class's constructor typescript: literal constructor references of a decorated class reference un-decorated class's constructor, when used inside decorated class Apr 27, 2024
@kdy1 kdy1 self-assigned this Apr 29, 2024
@kdy1 kdy1 added this to the Planned milestone Apr 29, 2024
@kdy1 kdy1 assigned kdy1 and unassigned kdy1 Apr 29, 2024
@benasher44
Copy link
Author

benasher44 commented Apr 29, 2024

One interesting note: still debugging this one, but there is a similar flavor of bug where static functions on the class don't get transformed this way. For example:

@MarkClass("example")
class Example {
  static create() {
    new Example()
  }
}

tsc creates an intermediary var so the emit looks like

let Example_1 = class Example {
  static create() {
    new Example_1()
  }
}
// apply decorate code here

Somehow this doesn't exhibit the same bug in tsc, but the viable workaround for swc is similar. We've updated our code to instead use new this(), in the static function. I guess somehow creating that Example_1 var allows makings this work by tsc — maybe something specific to tslib's decorate function? not sure though

@benasher44
Copy link
Author

I suppose swc could apply a similar transformation and also transform new Example() to new this(). Apologies; I haven't fully debugged this part yet, but I saw this issue was assigned and wanted to call out this part too, in case the assignee has a better handle on tslib's decorating than myself :)

@benasher44
Copy link
Author

benasher44 commented Apr 29, 2024

Ah okay so what tsc does is apply the decoration to Example and update Example_1 to point to the decorated class, to then calling Example.create() works as expected.

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

No branches or pull requests

2 participants