TypeScript: ES2022 Class Decorators not working when Class has self-static member
Bug Report
Class decorators are not working properly when the class has a static field of the same type and is instantiating it statically. Seems like this only happens when targeting ES2022.
🔎 Search Terms
Various combinations of the below search strings. Haven’t been able to find anything useful. Been searching for several days now, these are just the most recent ones I could remember:
- Typescript class decorator with static property not working
- Typescript decorators: TypeError: undefined is not a constructor
- Typescript decorators not working (with es2022)
- Typescript decorator es2022
- Angular custom decorators not working
- Angular custom decorators with aot
🕗 Version & Regression Information
Regressed when tsconfig.compilerOptions.target is ES2022. Other ES versions that I tried seem to be working as expected.
Tried Nightly version in the playground, could still repro it (after I changed target to ES2022 in TSConfig).
⏯ Playground Link
Bug repro in Playground You can try setting e.g. ES2021, and the code works, but not on ES2022.
💻 Code
src\index.ts:
function Message(ctor: Function)
{
console.log(`Registring: "${ctor.name}"`);
}
@Message
class PingMessage
{
public static Default: PingMessage = new PingMessage(); // [A]
public constructor(public readonly Value: string = "Ping") { }
}
// PingMessage.Default = new PingMessage(); // [B]
console.log(PingMessage.Default.Value);
// Swapping instantiation from [A] to [B] renders this code working.
// Seems like constructor is not defined well while instantiating a static member within its own class.
package.json:
{
"name": "decorator-test",
"version": "1.0.0",
"description": "Test App",
"scripts": {
"start": "(if exist dist rd /S /Q dist) >nul 2>nul && tsc && node ./dist/index.js"
},
"dependencies": {
"tslib": "^2.4.1"
},
"devDependencies": {
"typescript": "^4.9.4"
}
}
tsconfig.json:
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist",
"target": "ES2022", // <----- !!!
"module": "ES2022",
"strict": true,
"experimentalDecorators": true,
"useDefineForClassFields": false,
"lib": [
"ES2022",
"dom"
]
},
"files": [
"src/index.ts"
]
}
🙁 Actual behavior
~\dist\index.js:13 static { this.Default = new PingMessage_1(); } // [A] ^
TypeError: undefined is not a constructor at Function.<static_initializer> (~\dist\index.js:13:29) at Object.<anonymous> (~\dist\index.js:12:19) at Module._compile (node:internal/modules/cjs/loader:1155:14) at Object.Module._extensions…js (node:internal/modules/cjs/loader:1209:10) at Module.load (node:internal/modules/cjs/loader:1033:32) at Function.Module._load (node:internal/modules/cjs/loader:868:12) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12) at node:internal/main/run_main_module:22:47
🙂 Expected behavior
Console.logs:
Registring: "PingMessage"
Ping
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Reactions: 17
- Comments: 17 (6 by maintainers)
Came across this issue recently. I believe it is in fact a TS bug that needs to be resolved. The issue is caused by the aliased class name, which is not initialised at the time of the static initialisers being called, where the class itself is, in fact, available from within the static initialisers in ES2022.
Given the following TS Code:
If this is run targeting
ES2021or earlier then the class is available to the static initialisers for bothUndecoratedClassandDecoratedClass. But, if we targetES2022or later then theDecoratedClassclass receivesundefinedas its reference to itself from within its static initialisers. This is a result of the intermediary..._1class reference in the generated code not being populated yet:When there is no decorator, then the intermediary reference is not created and it all works as expected.
Here is a playground of the above example: https://www.typescriptlang.org/play?target=9#code/GYVwdgxgLglg9mABBATgUwIZTQMRXAWwGEAbDAZ3IAoIMSS0UAuRcqFGMAcwBpkzKAJTTAW4ANZg4AdzABKRAG8AUIjXIE5OAwB0JOFyoADACSLa9RgF8mRvhAHlhwOQG5V69FBAokAcmA4OD93K2VlUEhYBEQAZUI0ABE0CDgULDSqORZSCnJk1PSoNKUPNS8fJCzEAF4APiVEK1DwhzzEAFUwABMUtKw0btzKUvVWKCwYCERsNlrkdAG8QmHqPy7ewoGhxz8+Db6iwdW3ZTDlAAF4giTDjJQs5TaRgv7sHfaVMbZJ6dmoeaoTDYZbERxUPyvI4fSh7RBQ7YnUJAA
Proposed solution
Not sure all the reasons for the intermediary class reference, but potentially an alternative would be for TS to generate the following JS code when a decorator is used:
The code above effectively uses a shadowed variable name within the IIFE as the intermediary class reference to the same effect, but without the reference initialisation timing issue. This even allows for less of a need for modification of the decorated class code and the final code has fewer characters (ignoring whitespace). @rbuckton What do you think of this proposed approach for legacy decorators?
(EDIT: It could even be trimmed down to the following, but it really depends on the reason for the intermediary reference as to how far we simplify this.)
Will this be backported to 4.9? We are running into this issue in Angular 15 which uses TS 4.9 and targets ES2022 by default.
I am seeing more and more Angular projects running into this issue. @rbuckton is there any plan to address this issue soon? Hopefully my proposed solution above could provide some ideas.
Ok, I think I have got a solution that will work for all scenarios. I reworked the playground to make the expectations clearer too: See here
Trimmed TS example code:
When run using ES2021, the output is:
All expectations align.
But, when run using ES2022, the output is:
Many cases are broken for ES2022.
Here is the current code emitted for the ES2022 target:
I think the best solution that covers all bases here is to add a static initialiser block at the top of the emitted class:
Now the alias will be available to all the static initialiser blocks. Everything works!
Here is an example of the tweaked JS output code (playground) with just this one line added:
@rbuckton what do you think of this solution?
PS. potentially the line:
let Original = Original_1 = class Original {could now be simplified:
let Original = class Original {because the alias is assigned internally. Although this is just a bundle size optimization.
I agree that a backport would be really great. This bug is coming up many times in the Angular community.
This is still an issue with Angular 16.1.4 and Typescript 5.0.4 and a tsconfig with lib: es2022, module: es2022 and target of es2022. It’s fine with a target of es2021.
According to the Angular upgrade matrix (https://gist.github.com/LayZeeDK/c822cc812f75bb07b7c55d07ba2719b3), for Angular 16.x.x, you must use Typescript >=4.9.5 <5.1.0
Has anyone tried Typescript >=5.1.0 with Angular ^16.1.4?
If no one knows the compatibility results, I’ll give it a try and see what happens.
In general we only backport critical crashes or regression specific to a particular release, or issues without any workarounds. This doesn’t appear to qualify for any of those criteria.