#18223·babel

Decorators throw when TypeScript removes their memoization host field

Author: nyan-leftCreated Sep 4, 2026Updated Sep 4, 2026
Labelsi: needs triage

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):

typescript
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

json
{
  "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 declare variant above
  • Node 24.12.0, npm 11.12.1, pnpm 10.33.0
  • macOS 15.3.2, not a monorepo
  • Reproduced with @babel/core and @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.