Skip to content

useDefineForClassFields defaults to true when target is es2022, breaks decorators. #48814

Description

Bug Report

useDefineForClassFields defaults to true when target is set to es2022 or esnext. This is breaking lots of projects that use decorators.

This was reported in #45584, but that issue was closed apparently because the title didn't include which versions of TypeScript set useDefineForClassFields and because the TypeScript playground doesn't exhibit this same behavior. I've included a reproduction for tsc here.

This behavior is not documented anywhere in the TypeScript docs that I can find.

🔎 Search Terms

useDefineForClassFields decorators

🕗 Version & Regression Information

Since 4.2.4? according to #45584
Present in 4.6.3 and 4.7.0-dev.20220422

⏯ Playground Link

This playground link does not demonstrate the problem, as it doesn't default useDefineForClassFields like tsc does. But if you set useDefineForClassFields to true you will see that that decorators could not work because the field is not an instance property that shadows the decorator defined prototype accessors.

Playground link with relevant code

This git repo reproduces the problem locally: https://github.com/justinfagnani/ts-decorators-bug

💻 Code

This does not work with target es2022:

const observe = (proto: Object, name: PropertyKey) => {
  const key = Symbol();
  Object.defineProperty(proto, name, {
    get() {
      return this[key];
    },
    set(v: unknown) {
      this[key] = v;
      this.onChange?.(name, v);
    }
  });
};


class A {
  @observe
  foo = 'abc';

  onChange(name: string, v: unknown) {
    console.log(`${name} changed to ${v}`);
  }
}

const a = new A();
a.foo = 'x'

🙁 Actual behavior

useDefineForClassFields defaults to true

🙂 Expected behavior

Option 1: useDefineForClassFields defaults to false like for other permutations of module and target until the new behavior is documented and users are notified with a prominent breaking change notice on the decorators documentation and the release notes.

Option 2: Use constructor initialization for decorated properties even if useDefineForClassFields is true. This will keep projects from breaking regardless of the config.

Option 3: Disallow both useDefineForClassFields: true and experimentalDecorators in the same config.

This behavior is breaking a large number of our users. Sometimes they are in control of their own tsconfig and can set useDefineForClassFields to false, but sometimes the config is generated for them via some other tool and they don't know how to set the value. In this case their project is just broken and they blame our library, not the update to TypeScript that included the undocumented breaking change.

Activity

  1. andrewbranch commented on Apr 22, 2022

    @andrewbranch
    Member

    This behavior is not documented anywhere in the TypeScript docs that I can find.

    In the tsconfig reference:

    image

    And in the release notes:

    image

    Open to adding something to the Decorators docs. We discussed other options in a design meeting and the only one we were sort of ok with was issuing a config diagnostic for experimentalDecorators. (In fact, this is exactly what I said here.)

  2. justinfagnani commented on Apr 22, 2022

    @justinfagnani
    Author

    I guess I missed those and was looking in the decorator docs, don't know why I didn't see the 4.3 notes mention.

    Why can't decorated fields opt-out of define semantics? We're seeing more and more users that are broken, and they get no errors at all. They're just assuming our library broke when it didn't change.

  3. andrewbranch commented on Apr 22, 2022

    @andrewbranch
    Member

    Unfortunately the design notes only capture “That sounds horrifying to me” in answer to that question. From my memory, I think the sentiment was generally that experimental decorators are experimental, so we’re not going to break standards compliance elsewhere to make them keep working, and that users should simply not try to use them in combination with useDefineForClassFields, making the config diagnostic the only sensible mitigation.

  4. rbuckton commented on May 17, 2022

    @rbuckton
    Contributor

    useDefineForClassFields doesn't break "decorators", per se, they break this specific use of a decorator. Native decorators, per the Stage 3 spec, also cannot replace a field with an accessor. Instead, the Stage 3 decorators proposal introduces a new accessor keyword that performs the syntactic transformation of a field into an accessor for you:

    class C {
      accessor foo = 'abc';
    }
    // becomes
    class C {
      #foo_backing_store = 'abc';
      get foo() { return this.#foo_backing_store; }
      set foo(v) { this.#foo_backing_store = v; }
    }

    It will be a bit before TS support for Stage 3 Decorators is ready, but I have been considering introducing the accessor keyword to TS independently.

  5. rbuckton commented on May 17, 2022

    @rbuckton
    Contributor

    The other approach would be to recommend your users use declare if they have no control over the useDefineForClassFields setting:

    class C {
      @observe
      declare foo: string; // field not declared on class, but decorator will still run
    
      constructor() {
        this.foo = "abc";
      }
    }

    declare on a field in TS does not introduce a runtime field declaration under useDefineForClassFields, it merely adds it to the type in TS. A declare field cannot have an initializer, however, so it must be moved to the body of the constructor.

  6. justinfagnani commented on May 17, 2022

    @justinfagnani
    Author

    useDefineForClassFields breaks code that otherwise works with it set to false. Whether that's technically breaking decorators or not is pretty inside baseball that isn't so useful to our customers.

    I know that we can tell users to use declare if they hit this problem, but we also tell them to set useDefineForClassFields to false. This doesn't help if they don't make their way to that documentation. More often their components silently don't work and they don't know why.

  7. rbuckton commented on May 17, 2022

    @rbuckton
    Contributor

    A property descriptor isn't provided to a field decorator because there's no descriptor to modify. This decorator breaks because it uses Object.defineProperty to introduce a new property that ends up shadowed by the field. Unfortunately, this was never something field decorators were designed to do. TS field decorators primarily existed as a means of recording metadata about a field. Performing mutations on the prototype outside of that capability is unsafe.

  8. added
    Working as IntendedThe behavior described is the intended behavior; this is not a bug
    and removed
    SuggestionAn idea for TypeScript
    on May 18, 2022
  9. typescript-bot commented on May 21, 2022

    @typescript-bot
    Contributor

    This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  10. justinfagnani commented on May 22, 2022

    @justinfagnani
    Author

    This is really unfortunate, IMO. The switching of this setting to default true in some cases is actively breaking our users.

  11. andrewbranch commented on May 23, 2022

    @andrewbranch
    Member

    I tried to get some movement on this in another design meeting. I asked Ron Buckton (@rbuckton) to update this issue with the summary of what we talked about; it looks like the design meeting notes aren’t up yet.

  12. locked as resolved and limited conversation to collaborators on Oct 22, 2025
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

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions