Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Property initializers output for non-initialized types #45076

Open
nonara opened this issue Jul 16, 2021 · 4 comments
Open

Property initializers output for non-initialized types #45076

nonara opened this issue Jul 16, 2021 · 4 comments
Labels
Docs The issue relates to how you learn TypeScript

Comments

@nonara
Copy link

nonara commented Jul 16, 2021

Bug Report

This is technically not a bug, given ES Spec, however, I'm filing an issue in case an consideration needs to be made on how to handle with regard to documentation, etc, and/or to provide a solution for people facing the same.

Issue Summary

  • ESNext spec seems to call for defining all class fields. This also affects those which are optional or specified with a boom.
  • Spec also dictates that property initializers are called after the super call. (https://github.com/tc39/proposal-class-fields)

This results in any properties specified in the inherited class that are assigned during the base class constructor to be overwritten, even if they do not have initializer values specified in the inherited class.

The behaviour which causes the error can be seen below:

class A {
  a?: string
}

Output is:

// target = ESNext
class A {
  a; // Outputs a statement without initialized value, which is treated as a = undefined;
}

// target = ES2020
class A {
}

Here is a simplified version of how this affected me

abstract class Base<T extends Record<string, any>> {
  // Single constructor for all derivatives
  constructor(o: Omit<T, typeof Base>) {
    Object.assign(this, o);
  }
}

class A extends Base<A> {
  myProp?: string
  myProp2!: string
}

const a = new A({ myProp2: 'hello' });

// ESNext: a = { myProp2: undefined, myProp: undefined }
// ES2020: a = { myProp2: 'hello' }

The above produces:

// TypeScript target=ESNext
class A extends Base {
  myProp;
  myProp2; // Note the boomed property still gets output here, so it is initialized after the super call - which maybe surprising behaviour
}

// ES2020 / Babel
class A extends Base {
}

🔎 Search Terms

  • property initializers

🕗 Version & Regression Information

TS 4.3.5

Solution

For those facing this issue, simply change property declarations to ambient.

ie:

class A extends Base<A> {
  declare myProp?: string
  declare myProp2: string
}
@jcalz
Copy link
Contributor

jcalz commented Jul 18, 2021

Relevant: the --useDefineForClassFields compiler flag is what controls this behavior, and this behavior is documented since TypeScript 3.7. From TypeScript 3.7 until TypeScript 4.2, this compiler flag was disabled by default in all circumstances.

It seems that starting in TypeScript 4.3, this compiler option is now enabled by default if your target is ESNext. (See #42663 for implementation). This is likely what you are running into here. But, according to this comment in #34787, this change is not documented anywhere in the release notes for TypeScript 4.3.

So possibly this bug report is a request that the TypeScript 4.3 release notes should mention this as a breaking change? (And maybe it belongs on the TypeScript-website issue list instead of here?)

@nonara
Copy link
Author

nonara commented Jul 18, 2021

It seems that starting in TypeScript 4.3, this compiler option is now enabled by default if your target is ESNext. (See #42663 for implementation).

Ah! That's the culprit. Thanks.

So possibly this bug report is a request that the TypeScript 4.3 release notes should mention this as a breaking change?

That seems reasonable. I see several people commented that they ran into it as well, and it took some time to track down. It sidelined a good chunk of my day, also, when I bumped TS versions. Was a little tricky to figure out what was happening.

Adding to the documentation is probably a good idea. Looks like the issue was flagged as breaking change also, so it probably was intended to be in the notes

@MartinJohns
Copy link
Contributor

It seems that starting in TypeScript 4.3, this compiler option is now enabled by default if your target is ESNext. (See #42663 for implementation). This is likely what you are running into here. But, according to this comment in #34787, this change is not documented anywhere in the release notes for TypeScript 4.3.

To be fair, "ESNext" is not really a stable target and is breaking by definition. :-)

@fatcerberus
Copy link

For what it’s worth: useDefineForClassFields makes sense to enable by default when targeting ESNext, since otherwise behavior for each property may differ depending on whether a property has an initializer or not (esnext implies no transpilation and native class fields use [[Define]] semantics).

The change definitely should be documented, though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Docs The issue relates to how you learn TypeScript
Projects
None yet
Development

No branches or pull requests

5 participants