Skip to content

--experimentalDecorators: decorated abstract property is emitted as a class field (regression in 7.0) #64980

Open

Description

@simleb

🔎 Search Terms

experimentalDecorators, abstract property, decorator, class field, useDefineForClassFields, typeeraser, legacy decorators

🕗 Version & Regression Information

  • Regression from tsc 6.0.3, which erases the field.
  • Reproduces in 7.0.2, 7.1.0-beta and 7.1.0-dev.20261026.1.

💻 Code

function dec(target: object, key: string) {
  Object.defineProperty(target, key, { get: () => 'from prototype', configurable: true });
}

abstract class Base {
  @dec abstract data: string;
}

class Sub extends Base {
  declare data: string;
}

const s = new Sub();
console.log(Object.hasOwn(s, 'data'), s.data);
{ "compilerOptions": { "target": "es2022", "experimentalDecorators": true } }

🙁 Actual behavior

Base gets a data; field, so every instance has an own data: undefined that shadows whatever the decorator put on the prototype. Prints true undefined.

class Base {
    data;
}
__decorate([
    dec
], Base.prototype, "data", void 0);

🙂 Expected behavior

Same as tsc 6.0.3: the field is erased and only the __decorate call remains. Prints false from prototype.

class Base {
}
__decorate([
    dec
], Base.prototype, "data", void 0);

Additional information about the issue

The field appears only when all three hold: experimentalDecorators, a decorator on the abstract property, and define semantics (useDefineForClassFields, the default at target ES2022 and later). emitDecoratorMetadata makes no difference. An undecorated abstract property and a decorated declare property are both still erased.

The likely cause is in typeeraser.go. For a decorated declare/abstract property under experimentalDecorators, it keeps the node so that LegacyDecoratorsTransformer can emit the __decorate call. But it rebuilds the node with VisitModifiers, which elides AbstractKeyword. visitPropertyDeclaration in legacydecorators.go then finds neither NodeFlagsAmbient nor ModifierFlagsAbstract and emits a field. declare escapes this because the node keeps NodeFlagsAmbient. Strada's visitPropertyDeclaration in ts.ts handles the same case by appending a declare modifier to the preserved node (concatenate(modifiers, factory.createModifiersFromModifierFlags(ModifierFlags.Ambient))).

We hit this in TypeORM entities: an abstract base declares a decorated abstract column, each subclass redeclares it with declare, and every entity instance gains an own data: undefined it doesn't have under tsc 6.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions