swc
swc copied to clipboard
typescript: literal constructor references of a decorated class reference un-decorated class's constructor, when used inside decorated class
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.
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.
All the swc would need to do in case of a decorated class, is to copycat the tsc behavior, which itself slaps a workaround on to mitigate the issue.
so instead of:
class Example {
copy() {
return new Example();
}
}
Example = _ts_decorate([
Do:
let Example_1; // or _Example, or whatever you prefer
class Example {
static { Example_1 = this; } // yes, that's what tsc does when a class is decorated
copy() {
return new Example_1(); // and replace all static references inside the class with the alias
}
}
Example_1 = Example = _ts_decorate([
This would also fix other related issues i.a. https://github.com/swc-project/swc/issues/6515, https://github.com/swc-project/swc/issues/8039 @kdy1