typescript: literal constructor references of a decorated class reference un-decorated class's constructor, when used inside decorated class
benasher44 opened this issue · comments
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)
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.
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
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 :)
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.