The useDefineForClassFields Flag and The declare Property Modifier

Back when TypeScript implemented public class fields, we assumed to the best of our abilities that the following code

  1. class C {
  2. foo = 100;
  3. bar: string;
  4. }

would be equivalent to a similar assignment within a constructor body.

  1. class C {
  2. constructor() {
  3. this.foo = 100;
  4. }
  5. }

Unfortunately, while this seemed to be the direction that the proposal moved towards in its earlier days, there is an extremely strong chance that public class fields will be standardized differently.Instead, the original code sample might need to de-sugar to something closer to the following:

  1. class C {
  2. constructor() {
  3. Object.defineProperty(this, "foo", {
  4. enumerable: true,
  5. configurable: true,
  6. writable: true,
  7. value: 100
  8. });
  9. Object.defineProperty(this, "bar", {
  10. enumerable: true,
  11. configurable: true,
  12. writable: true,
  13. value: void 0
  14. });
  15. }
  16. }

While TypeScript 3.7 isn’t changing any existing emit by default, we’ve been rolling out changes incrementally to help users mitigate potential future breakage.We’ve provided a new flag called useDefineForClassFields to enable this emit mode with some new checking logic.

The two biggest changes are the following:

  • Declarations are initialized with Object.defineProperty.
  • Declarations are always initialized to undefined, even if they have no initializer.This can cause quite a bit of fallout for existing code that use inheritance. First of all, set accessors from base classes won’t get triggered - they’ll be completely overwritten.
  1. class Base {
  2. set data(value: string) {
  3. console.log("data changed to " + value);
  4. }
  5. }
  6. class Derived extends Base {
  7. // No longer triggers a 'console.log'
  8. // when using 'useDefineForClassFields'.
  9. data = 10;
  10. }

Secondly, using class fields to specialize properties from base classes also won’t work.

  1. interface Animal { animalStuff: any }
  2. interface Dog extends Animal { dogStuff: any }
  3. class AnimalHouse {
  4. resident: Animal;
  5. constructor(animal: Animal) {
  6. this.resident = animal;
  7. }
  8. }
  9. class DogHouse extends AnimalHouse {
  10. // Initializes 'resident' to 'undefined'
  11. // after the call to 'super()' when
  12. // using 'useDefineForClassFields'!
  13. resident: Dog;
  14. constructor(dog: Dog) {
  15. super(dog);
  16. }
  17. }

What these two boil down to is that mixing properties with accessors is going to cause issues, and so will re-declaring properties with no initializers.

To detect the issue around accessors, TypeScript 3.7 will now emit get/set accessors in .d.ts files so that in TypeScript can check for overridden accessors.

Code that’s impacted by the class fields change can get around the issue by converting field initializers to assignments in constructor bodies.

  1. class Base {
  2. set data(value: string) {
  3. console.log("data changed to " + value);
  4. }
  5. }
  6. class Derived extends Base {
  7. constructor() {
  8. data = 10;
  9. }
  10. }

To help mitigate the second issue, you can either add an explicit initializer or add a declare modifier to indicate that a property should have no emit.

  1. interface Animal { animalStuff: any }
  2. interface Dog extends Animal { dogStuff: any }
  3. class AnimalHouse {
  4. resident: Animal;
  5. constructor(animal: Animal) {
  6. this.resident = animal;
  7. }
  8. }
  9. class DogHouse extends AnimalHouse {
  10. declare resident: Dog;
  11. // ^^^^^^^
  12. // 'resident' now has a 'declare' modifier,
  13. // and won't produce any output code.
  14. constructor(dog: Dog) {
  15. super(dog);
  16. }
  17. }

Currently useDefineForClassFields is only available when targeting ES5 and upwards, since Object.defineProperty doesn’t exist in ES3.To achieve similar checking for issues, you can create a seperate project that targets ES5 and uses —noEmit to avoid a full build.

For more information, you can take a look at the original pull request for these changes.

We strongly encourage users to try the useDefineForClassFields flag and report back on our issue tracker or in the comments below.This includes feedback on difficulty of adopting the flag so we can understand how we can make migration easier.