Decorators throw when TypeScript removes their memoization host field
AI disclosure: The text below was drafted with assistance from an LLM.
- Would you like to work on a fix?
How are you using Babel?
Programmatic API (babel.transform, babel.parse)
Input code
On Babel 7, this fails with the default TypeScript preset behavior (allowDeclareFields: false):
const seen: string[] = [];
const lib = {
zUI: () => (_value: unknown, context: { name: string | symbol }) => {
seen.push(String(context.name));
},
zObserve: (_fn: unknown) => (
_value: unknown,
context: { name: string | symbol },
) => {
seen.push(String(context.name));
},
};
class Example {
untouched: string;
@lib.zUI()
@lib.zObserve(() => {})
target = 2;
}
new Example();
console.assert(seen.join() === "target,target");On Babel 8, changing the first field to declare untouched: string; reproduces the same failure.
Configuration file name
No response
Configuration
{
"presets": ["@babel/preset-typescript"],
"plugins": [
["@babel/plugin-proposal-decorators", { "version": "2023-11" }]
]
}Current and expected behavior
The generated class throws TypeError: A decorator must be a function. Babel moves the memoized decorator-array assignment into the computed key of an unrelated field, then the TypeScript transform removes that field and the assignment with it.
The decorator expressions should still be evaluated in source order and applied to target, even when another class field is removed by a later transform.
Environment
- Babel 7.25.9 and 7.29.8; Babel 8.0.4 with the
declarevariant above - Node 24.12.0, npm 11.12.1, pnpm 10.33.0
- macOS 15.3.2, not a monorepo
- Reproduced with
@babel/coreand@babel/standalone
Possible solution
The fallback in packages/babel-helper-create-class-features-plugin/src/decorators.ts, in transformClass, prepends leftover computedKeyAssignments to the first public class element. That host can be a field that a later TypeScript visitor removes.
The fallback should skip fields without initializers, including declare and abstract fields. If no durable public element exists, it can use the existing temporary computed field instead.
Additional context
Plain field: Type; is valid TypeScript. Babel 7 may intentionally erase it when allowDeclareFields is false, but required decorator evaluation must not be attached to an element that can disappear. Babel 8 has the equivalent failure with erased declaration fields.
Source: babel/babel