`__setFunctionName` breaks custom static `name` on classes
🔎 Search Terms
__setFunctionName class static name ES decorators static name __esDecorate static name
🕗 Version & Regression Information
5.0.4-5.9.3
⏯ Playground Link
https://tsplay.dev/WJgblw
💻 Code
const noop = (Self: any, ctx: ClassDecoratorContext) => {};
const rand = () => 4;
@noop export default class {
static get name() { return 2434; }
}
(@noop class {
static get name() { return 2434; }
});
(@noop class __A {
static get name() { return 2434; }
});
@noop
class __B {
static get name() { return 2434; }
}
var __C = @noop class {
static get name() { return 2434; }
};
var __D = {
[rand()]: @noop class {
static get name() { return 2434; }
}
};
// Unconditionally throws at runtime:
@noop class __E {
static #a = 2434;
static get name() { return this.#a; }
}
// Undecorated classes are broken as well:
var __F = class {
static get name() { return 2434; }
// target≤es2021 => '__F'
// target≥es2022 => 2434
static {}
}
🙁 Actual behavior
Static name is overridden by __setFunctionName().
🙂 Expected behavior
Static name should work as defined in the class code.
Additional information about the issue
Generated code for the ES decorators breaks custom static name property on the decorated class. The only syntax that works is plain static name; field declaration; other kinds (methods, get/set/accessor) are broken.
The issue reproduces iff the compiler decides to inject the static { __setFunctionName(this, ...) } block, which unconditionally overrides the static name descriptor on the class object.
The issue also affects undecorated classes (when targeting older environments; see the examples).
I’m not sure what exactly triggers the
__setFunctionName()block injection. Sometimes it’s injected even if the class has a correct unambiguous automatic name. Sometimes it isn’t injected, even if the class becomes incorrectly named (e.g., when targetinges3/es5):Missing `__setFunctionName()`
https://tsplay.dev/WKYAMN
// tests/cases/compiler/blockScopedVariablesUseBeforeDef.ts function foo8() { let y = class { a = x; }; let x; // @ts-ignore console.log(y.name); } // target≤es5 => 'class_###' // target≥es2015 => 'y' foo8();But either way, it shadows non-field static
namedeclarations.
Possible Fix
At runtime, check that the descriptor of this.name matches the automatic name descriptor shape (i.e., value:string, writable≡enumerable≡false, configurable=true). The runtime check is necessary to correctly handle static name declared with a computed key.
Technically, the necessary and sufficient condition is even simpler—just check for writable≡false before __setFunctionName to robustly prevent overriding of non-field static name declarations:
- At this point, the descriptors are completely defined by the syntax:
- No
static {}blocks evaluated. - No class decorators evaluated.
- No class element initializers evaluated.
- No
- So, the static
nameis one of:- Automatic, always defined. If there’s a static
namefield, it’s to be reconfigured at the time of the actual field initialization. - Method.
-
get/setoraccessor.
- Automatic, always defined. If there’s a static
- Methods have
writable≡true. -
get/set/accessordeclarations havewritable≡undefined.
This matches the correct behavior:
- With a field
static namedeclaration, it’s the automaticnameup to the time of the actualstatic nameinitialization. The field initialization successfully overrides the automatic descriptor. - For non-fields, the descriptor matches the declaration from the very beginning of the class. If the
nameis reconfigured here, the actual declaration doesn’t revert the override.
Adjust createClassNamedEvaluationHelperBlock() and isClassNamedEvaluationHelperBlock() in namedEvaluation.ts to produce a code like this:
static {
Object.getOwnPropertyDescriptor(this, "name").writable === false &&
__setFunctionName(this, ...);
}
NB: With
target≤es5anduseDefineForClassFields≡false, the compiler uses plainC.name=...assignment to set the staticname, which has no effect (doesn’t reconfigure the staticnamedescriptor), so the injected block—if injected—will call__setFunctionName()unconditionally. But this is already a compile-time errorStatic property 'name' conflicts with built-in property 'Function.name' of constructor function 'A'. (2699)so it doesn’t matter here.
🤖 Thank you for your issue! I've done some analysis to help get you started. This response is automatically generated; feel free to 👍 or 👎 this comment according to its usefulness.
Similar Issues
Here are the most similar issues I found
- (72%) microsoft/typescript#44908: Bad esnext emit for static property that references another static property when the class has a decorator
- (71%) microsoft/typescript#54193: Impossible to define static property `name` in classes
- (69%) microsoft/typescript#5386: Class name is missing in Function when es6 target is set
- (67%) microsoft/typescript#9778: Emitter: Static class property "name" causes TypeError due to Function.name being readonly
- (67%) microsoft/typescript#8074: [Transforms] References to decorated classes in static functions does not refer to the renamed entity
- (67%) microsoft/typescript#53218: When the compile target is set to ES2022, an error will be reported at runtime
- (67%) microsoft/typescript#52004: ES2022 Class Decorators not working when Class has self-static member
- (67%) microsoft/typescript#4143: TypeScript should error on static attribute called 'name'
- (67%) microsoft/typescript#3190: Do not allow to define a static `name` property on classes
- (67%) microsoft/typescript#37157: Class name not being inherited in anonymous classes results in decorated classes losing their original name with Node 12.16.0 and above
- (66%) microsoft/typescript#7168: Metadata/Reflection ES6: Decorated classes are generated without class name; "target.name" is empty in class decorator
- (65%) microsoft/typescript#6132: Missing class name when use decorator
- (65%) microsoft/typescript#16141: Methods in classes are anonymous function on es3/es5 target
If your issue is a duplicate of one of these, feel free to close this issue. Otherwise, no action is needed.
Possibly Working as Intended
It looks like this behavior might be intentional. Here's what a computer had to say about it.
Hi @RyanCavanaugh , please review this PR, thanks