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

Inconsistent parameter properties initialization order depending on the target syntax with useDefineForClassFields #45995

Open
nicolo-ribaudo opened this issue Sep 21, 2021 · 8 comments
Assignees
Labels
Breaking Change Would introduce errors in existing code Domain: JS Emit The issue relates to the emission of JavaScript Rescheduled This issue was previously scheduled to an earlier milestone

Comments

@nicolo-ribaudo
Copy link

Bug Report

🔎 Search Terms

properties initialization esnext order initialization fields useDefineForClassFields

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about classes

Ref: babel/babel#13779

⏯ Playground Link

Playground link with relevant code (ES2021 target)

Playground link with relevant code (ESNext target)

💻 Code

new class Dashboard {
    name = console.log(this.amount);
    constructor(public amount: number) {
    }
}(1);

🙁 Actual behavior

When targeting ES2021 this.amount is initialized first; when targeting ESNext this.name is initialized first.

🙂 Expected behavior

They should be consistent.

@andrewbranch andrewbranch added the Needs Investigation This issue needs a team member to investigate its status. label Sep 23, 2021
@andrewbranch andrewbranch added this to the TypeScript 4.6.0 milestone Sep 23, 2021
@dnalborczyk
Copy link

dnalborczyk commented Oct 4, 2021

@nicolo-ribaudo esnext (and eventually es2022) ignore useDefineForClassFields as there is no down leveling.

for target: es2021 and useDefineForClassFields: false I would expect the compiler to generate non-ecmascript standard with the wrong property initialization order and using this, and to throw an Error with useDefineForClassFields: true.

I think the name useDefineForClassFields is unfortunately a bit misleading, as it not only uses defineProperty but also fixes the initialization order.

I think useDefineForClassFields should be the default for new Typescript projects (tsc --init) and also be recommended in the docs. People will get a big awakening once es2022 becomes the default and their code all the sudden behaves differently.

@dnalborczyk
Copy link

for target: es2021 and useDefineForClassFields: false I would expect the compiler to generate non-ecmascript standard with the wrong property initialization order and using this, and to throw an Error with useDefineForClassFields: true.

that said, if this can be fixed, even if it's essentially "breaking", even better.

@RyanCavanaugh RyanCavanaugh added the Rescheduled This issue was previously scheduled to an earlier milestone label May 13, 2022
alan-agius4 added a commit to alan-agius4/angular-cli that referenced this issue Sep 20, 2022
…MA output

With this change we reduce the reliance on the TypeScript target compiler option to output a certain ECMA version. Instead we now use the browsers that are configured in the Browserslist configuration to determine which ECMA features and version are needed.
This is done by passing the transpiled TypeScript to Babel preset-env.

**Note about useDefineForClassFields**: while setting this to `false` will output JavaScript which is not spec compliant, this is needed because TypeScript introduced class fields many years before it was ratified in TC39. The latest version of the spec have a different runtime behavior to TypeScript’s implementation but the same syntax. Therefore, we opt-out from using upcoming ECMA runtime behavior to better support the ECO system and libraries that depend on the non spec compliant output. One of biggest case is usages of the deprected `@Effect` decorator by NGRX which otherwise would cause runtime failures. Dropping `useDefineForClassFields` will be considered in a future major releases. For more information see: microsoft/TypeScript#45995.

BREAKING CHANGE: Internally. the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration.
alan-agius4 added a commit to alan-agius4/angular-cli that referenced this issue Sep 20, 2022
…MA output

With this change we reduce the reliance on the TypeScript target compiler option to output a certain ECMA version. Instead we now use the browsers that are configured in the Browserslist configuration to determine which ECMA features and version are needed.
This is done by passing the transpiled TypeScript to Babel preset-env.

**Note about useDefineForClassFields**: while setting this to `false` will output JavaScript which is not spec compliant, this is needed because TypeScript introduced class fields many years before it was ratified in TC39. The latest version of the spec have a different runtime behavior to TypeScript’s implementation but the same syntax. Therefore, we opt-out from using upcoming ECMA runtime behavior to better support the ECO system and libraries that depend on the non spec compliant output. One of biggest case is usages of the deprected `@Effect` decorator by NGRX which otherwise would cause runtime failures. Dropping `useDefineForClassFields` will be considered in a future major releases. For more information see: microsoft/TypeScript#45995.

BREAKING CHANGE: Internally. the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration.
alan-agius4 added a commit to alan-agius4/angular-cli that referenced this issue Sep 20, 2022
…MA output

With this change we reduce the reliance on the TypeScript target compiler option to output a certain ECMA version. Instead we now use the browsers that are configured in the Browserslist configuration to determine which ECMA features and version are needed.
This is done by passing the transpiled TypeScript to Babel preset-env.

**Note about useDefineForClassFields**: while setting this to `false` will output JavaScript which is not spec compliant, this is needed because TypeScript introduced class fields many years before it was ratified in TC39. The latest version of the spec have a different runtime behavior to TypeScript’s implementation but the same syntax. Therefore, we opt-out from using upcoming ECMA runtime behavior to better support the ECO system and libraries that depend on the non spec compliant output. One of biggest case is usages of the deprected `@Effect` decorator by NGRX which otherwise would cause runtime failures. Dropping `useDefineForClassFields` will be considered in a future major releases. For more information see: microsoft/TypeScript#45995.

BREAKING CHANGE: Internally the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration.
alan-agius4 added a commit to alan-agius4/angular-cli that referenced this issue Sep 20, 2022
…MA output

With this change we reduce the reliance on the TypeScript target compiler option to output a certain ECMA version. Instead we now use the browsers that are configured in the Browserslist configuration to determine which ECMA features and version are needed.
This is done by passing the transpiled TypeScript to Babel preset-env.

**Note about useDefineForClassFields**: while setting this to `false` will output JavaScript which is not spec compliant, this is needed because TypeScript introduced class fields many years before it was ratified in TC39. The latest version of the spec have a different runtime behavior to TypeScript’s implementation but the same syntax. Therefore, we opt-out from using upcoming ECMA runtime behavior to better support the ECO system and libraries that depend on the non spec compliant output. One of biggest case is usages of the deprected `@Effect` decorator by NGRX which otherwise would cause runtime failures. Dropping `useDefineForClassFields` will be considered in a future major releases. For more information see: microsoft/TypeScript#45995.

BREAKING CHANGE: Internally the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration.
alan-agius4 added a commit to alan-agius4/angular-cli that referenced this issue Sep 20, 2022
…MA output

With this change we reduce the reliance on the TypeScript target compiler option to output a certain ECMA version. Instead we now use the browsers that are configured in the Browserslist configuration to determine which ECMA features and version are needed.
This is done by passing the transpiled TypeScript to Babel preset-env.

**Note about useDefineForClassFields**: while setting this to `false` will output JavaScript which is not spec compliant, this is needed because TypeScript introduced class fields many years before it was ratified in TC39. The latest version of the spec have a different runtime behavior to TypeScript’s implementation but the same syntax. Therefore, we opt-out from using upcoming ECMA runtime behavior to better support the ECO system and libraries that depend on the non spec compliant output. One of biggest case is usages of the deprected `@Effect` decorator by NGRX which otherwise would cause runtime failures. Dropping `useDefineForClassFields` will be considered in a future major releases. For more information see: microsoft/TypeScript#45995.

BREAKING CHANGE: Internally the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration.
alan-agius4 added a commit to alan-agius4/angular-cli that referenced this issue Sep 20, 2022
…MA output

With this change we reduce the reliance on the TypeScript target compiler option to output a certain ECMA version. Instead we now use the browsers that are configured in the Browserslist configuration to determine which ECMA features and version are needed.
This is done by passing the transpiled TypeScript to Babel preset-env.

**Note about useDefineForClassFields**: while setting this to `false` will output JavaScript which is not spec compliant, this is needed because TypeScript introduced class fields many years before it was ratified in TC39. The latest version of the spec have a different runtime behavior to TypeScript’s implementation but the same syntax. Therefore, we opt-out from using upcoming ECMA runtime behavior to better support the ECO system and libraries that depend on the non spec compliant output. One of biggest case is usages of the deprected `@Effect` decorator by NGRX which otherwise would cause runtime failures. Dropping `useDefineForClassFields` will be considered in a future major releases. For more information see: microsoft/TypeScript#45995.

BREAKING CHANGE: Internally the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration.
alan-agius4 added a commit to alan-agius4/angular-cli that referenced this issue Sep 20, 2022
…MA output

With this change we reduce the reliance on the TypeScript target compiler option to output a certain ECMA version. Instead we now use the browsers that are configured in the Browserslist configuration to determine which ECMA features and version are needed.
This is done by passing the transpiled TypeScript to Babel preset-env.

**Note about useDefineForClassFields**: while setting this to `false` will output JavaScript which is not spec compliant, this is needed because TypeScript introduced class fields many years before it was ratified in TC39. The latest version of the spec have a different runtime behavior to TypeScript’s implementation but the same syntax. Therefore, we opt-out from using upcoming ECMA runtime behavior to better support the ECO system and libraries that depend on the non spec compliant output. One of biggest case is usages of the deprected `@Effect` decorator by NGRX which otherwise would cause runtime failures. Dropping `useDefineForClassFields` will be considered in a future major releases. For more information see: microsoft/TypeScript#45995.

BREAKING CHANGE: Internally the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration.
alan-agius4 added a commit to alan-agius4/angular-cli that referenced this issue Sep 20, 2022
…MA output

With this change we reduce the reliance on the TypeScript target compiler option to output a certain ECMA version. Instead we now use the browsers that are configured in the Browserslist configuration to determine which ECMA features and version are needed.
This is done by passing the transpiled TypeScript to Babel preset-env.

**Note about useDefineForClassFields**: while setting this to `false` will output JavaScript which is not spec compliant, this is needed because TypeScript introduced class fields many years before it was ratified in TC39. The latest version of the spec have a different runtime behavior to TypeScript’s implementation but the same syntax. Therefore, we opt-out from using upcoming ECMA runtime behavior to better support the ECO system and libraries that depend on the non spec compliant output. One of biggest case is usages of the deprected `@Effect` decorator by NGRX which otherwise would cause runtime failures. Dropping `useDefineForClassFields` will be considered in a future major releases. For more information see: microsoft/TypeScript#45995.

BREAKING CHANGE: Internally the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration.
alan-agius4 added a commit to alan-agius4/angular-cli that referenced this issue Sep 20, 2022
…MA output

With this change we reduce the reliance on the TypeScript target compiler option to output a certain ECMA version. Instead we now use the browsers that are configured in the Browserslist configuration to determine which ECMA features and version are needed.
This is done by passing the transpiled TypeScript to Babel preset-env.

**Note about useDefineForClassFields**: while setting this to `false` will output JavaScript which is not spec compliant, this is needed because TypeScript introduced class fields many years before it was ratified in TC39. The latest version of the spec have a different runtime behavior to TypeScript’s implementation but the same syntax. Therefore, we opt-out from using upcoming ECMA runtime behavior to better support the ECO system and libraries that depend on the non spec compliant output. One of biggest case is usages of the deprected `@Effect` decorator by NGRX which otherwise would cause runtime failures. Dropping `useDefineForClassFields` will be considered in a future major releases. For more information see: microsoft/TypeScript#45995.

BREAKING CHANGE: Internally the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration.
alan-agius4 added a commit to alan-agius4/angular-cli that referenced this issue Sep 21, 2022
…MA output

With this change we reduce the reliance on the TypeScript target compiler option to output a certain ECMA version. Instead we now use the browsers that are configured in the Browserslist configuration to determine which ECMA features and version are needed. This is done by passing the transpiled TypeScript to Babel preset-env.

**Note about useDefineForClassFields**: while setting this to `false` will output JavaScript which is not spec compliant, this is needed because TypeScript introduced class fields many years before it was ratified in TC39. The latest version of the spec have a different runtime behavior to TypeScript’s implementation but the same syntax. Therefore, we opt-out from using upcoming ECMA runtime behavior to better support the ECO system and libraries that depend on the non spec compliant output. One of biggest case is usages of the deprecated `@Effect` decorator by NGRX and potentially other existing code as well which otherwise would cause runtime failures. Dropping `useDefineForClassFields` will be considered in a future major releases. For more information see: microsoft/TypeScript#45995.

BREAKING CHANGE: Internally the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration.
alan-agius4 added a commit to alan-agius4/angular-cli that referenced this issue Sep 21, 2022
…MA output

With this change we reduce the reliance on the TypeScript target compiler option to output a certain ECMA version. Instead we now use the browsers that are configured in the Browserslist configuration to determine which ECMA features and version are needed. This is done by passing the transpiled TypeScript to Babel preset-env.

**Note about useDefineForClassFields**: while setting this to `false` will output JavaScript which is not spec compliant, this is needed because TypeScript introduced class fields many years before it was ratified in TC39. The latest version of the spec have a different runtime behavior to TypeScript’s implementation but the same syntax. Therefore, we opt-out from using upcoming ECMA runtime behavior to better support the ECO system and libraries that depend on the non spec compliant output. One of biggest case is usages of the deprecated `@Effect` decorator by NGRX and potentially other existing code as well which otherwise would cause runtime failures. Dropping `useDefineForClassFields` will be considered in a future major releases. For more information see: microsoft/TypeScript#45995.

BREAKING CHANGE: Internally the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration.
clydin pushed a commit to angular/angular-cli that referenced this issue Sep 21, 2022
…MA output

With this change we reduce the reliance on the TypeScript target compiler option to output a certain ECMA version. Instead we now use the browsers that are configured in the Browserslist configuration to determine which ECMA features and version are needed. This is done by passing the transpiled TypeScript to Babel preset-env.

**Note about useDefineForClassFields**: while setting this to `false` will output JavaScript which is not spec compliant, this is needed because TypeScript introduced class fields many years before it was ratified in TC39. The latest version of the spec have a different runtime behavior to TypeScript’s implementation but the same syntax. Therefore, we opt-out from using upcoming ECMA runtime behavior to better support the ECO system and libraries that depend on the non spec compliant output. One of biggest case is usages of the deprecated `@Effect` decorator by NGRX and potentially other existing code as well which otherwise would cause runtime failures. Dropping `useDefineForClassFields` will be considered in a future major releases. For more information see: microsoft/TypeScript#45995.

BREAKING CHANGE: Internally the Angular CLI now always set the TypeScript `target` to `ES2022` and `useDefineForClassFields` to `false` unless the target is set to `ES2022` or later in the TypeScript configuration. To control ECMA version and features use the Browerslist configuration.
@RyanCavanaugh RyanCavanaugh removed this from the TypeScript 4.8.0 milestone Feb 1, 2023
@nicolo-ribaudo
Copy link
Author

@rbuckton I see that this issue has been added to the 5.1.0 milestone. Is there any update? Asking because we are aligning our behavior with TS in babel/babel#15583.

@nicolo-ribaudo
Copy link
Author

We got more issues by users confused about useDefineForClassFields affecting parameter properties, it would be great to move towards a decision for this issue :)

richvdh added a commit to matrix-org/matrix-js-sdk that referenced this issue Jun 21, 2024
It seems that ES2022 causes typescript to change the initialization order of
regular properties vs parameter properties
(microsoft/TypeScript#45995), so we need to rearrange
the initializations to avoid an error.

In practice, it might be fine because we have enabled
`babel-plugin-transform-class-properties`, which moves the initialization back
after the parameter property, but we shoudn't rely on that, and anyway it
upsets the linter.
github-merge-queue bot pushed a commit to matrix-org/matrix-js-sdk that referenced this issue Jun 21, 2024
* Bump ES target version to ES2022

I want to be able to use `WeakRef`, and per
element-hq/element-web#24913 (comment),
I believe this should be safe.

* room.ts: Fix initialisation order

It seems that ES2022 causes typescript to change the initialization order of
regular properties vs parameter properties
(microsoft/TypeScript#45995), so we need to rearrange
the initializations to avoid an error.

In practice, it might be fine because we have enabled
`babel-plugin-transform-class-properties`, which moves the initialization back
after the parameter property, but we shoudn't rely on that, and anyway it
upsets the linter.
github-merge-queue bot pushed a commit to matrix-org/matrix-js-sdk that referenced this issue Jun 21, 2024
* Bump ES target version to ES2022

I want to be able to use `WeakRef`, and per
element-hq/element-web#24913 (comment),
I believe this should be safe.

* room.ts: Fix initialisation order

It seems that ES2022 causes typescript to change the initialization order of
regular properties vs parameter properties
(microsoft/TypeScript#45995), so we need to rearrange
the initializations to avoid an error.

In practice, it might be fine because we have enabled
`babel-plugin-transform-class-properties`, which moves the initialization back
after the parameter property, but we shoudn't rely on that, and anyway it
upsets the linter.
github-merge-queue bot pushed a commit to matrix-org/matrix-js-sdk that referenced this issue Jun 21, 2024
* Bump ES target version to ES2022

I want to be able to use `WeakRef`, and per
element-hq/element-web#24913 (comment),
I believe this should be safe.

* room.ts: Fix initialisation order

It seems that ES2022 causes typescript to change the initialization order of
regular properties vs parameter properties
(microsoft/TypeScript#45995), so we need to rearrange
the initializations to avoid an error.

In practice, it might be fine because we have enabled
`babel-plugin-transform-class-properties`, which moves the initialization back
after the parameter property, but we shoudn't rely on that, and anyway it
upsets the linter.
@rbuckton
Copy link
Member

rbuckton commented Aug 12, 2024

If we were building the feature from scratch based on the current version of ES, then I would expect that field initializers would run before parameter property initializers given that field initializers don't have access to the constructor body:

// @target: esnext
// @useDefineForClassFields: trueclass C {
  x = 1;
  constructor(public y: number) {}
}

// becomes
class C {
  x = 1;
  y;
  constructor(y) {
    this.y = y;
  }
}

playground

This order also has the best semantics when combined with native class element decorators as well as the parameter decorators proposal.

Unfortunately, prior to useDefineForClassFields, TypeScript has historically set parameter property initializers first:

// @target: es2015
// @useDefineForClassFields: falseclass C {
  x = 1;
  constructor(public y: number) {}
}

// becomes
class C {
  constructor(y) {
    this.y = y;
    this.x = 1;
  }
}

playground

Since field initializers are allowed to access this, it is possible to write code that would break were we to align the emit order for useDefineForClassFields:

// @target: es2015
// @useDefineForClassFields: false
class C {
  x = this.y.x;
  constructor(public y: { x: number }) {}
}

// becomes
class C {
    constructor(y) {
        this.y = y;
        this.x = this.y.x;
    }
}

playground

Any change we make to emit will need to be carefully considered for the potential breaks it would introduce. So far, we've opted to leave the emit order distinct between the two cases under the premise that useDefineForClassFields: false is essentially a legacy option, especially since we will implicitly set it to true when your target is high enough.

@rbuckton
Copy link
Member

Per the design meeting, the current plan would be to add a deprecation for useDefineForClassFields: false in TS 6.0 and remove the flag entirely in 6.5. This would mean that eventually our behavior for pre-ES2022 would be to align with ES2022 semantics for fields, which would also address the ordering, but that will likely be at least two years out.

An initial investigation into the impact of this change was performed in #59623, which only looked for the "use-before-initialization" error illustrated by my third example, above. Only one instance of this error was found in the top 1000 projects, which is a hopeful sign. However, that investigation did not look at other possible issues resulting from side effects.

@rbuckton rbuckton added Breaking Change Would introduce errors in existing code Domain: JS Emit The issue relates to the emission of JavaScript and removed Needs Investigation This issue needs a team member to investigate its status. labels Aug 15, 2024
@JoostK
Copy link
Contributor

JoostK commented Sep 9, 2024

As an additional data point, I have applied the change in #59623 to a private repo (~500kLOC) which heavily depends on the "old" initialization order (i.e. useDefineForClassFields: false) and no new diagnostics are reported, so I doubt it's a good proxy to measure the impact of changing the initialization order.

@NicBright
Copy link

NicBright commented Sep 19, 2024

@JoostK @rbuckton this is how it looks like in our project (around 79k statements according to our code coverage report, approx. 340k LOC (excluding tests) + 497k LOC (tests only - approx. 14k test cases)):

Starting with a 0-error-compiling project, when setting target: 'es2022' + useDefineForClassFields: true we suddenly get 1336 errors in 538 files:

image

Possible migration paths

Let's explore, what migration would look like for us. I could think of four different solutions. However, any of those is taking a lot of flexibility away and they all have significant drawbacks. It just feels like a big step back regarding JS ergonomics:

1) The Super Class Hack

This is maybe the best option for singleton services that are used in a lot of different places because the consumers don't need to be updated:
image

2) Conversion Into Functions / Methods

This could be suitable for components and services dedicated to a component. In this case, there will be only few and obvious places that need to be updated:

image

3) Moving Things Into The Constructor

This will easily lead to constructor bloat, especially if you would like to continue to have TS to infer the type of private (maybe even public) properties (in this case, you can't delegate it to separate private methods, because then, type inference will not work any more). In the screenshot, admittedly, we don't use type inference, but some people clearly prefer it over explicitly adding types to private properties.

image

4) Switching To Inject() - Angular specific

This is the easiest migration path for projects that use (the really slow) TestBed for everything, even for testing simple services.

It would mean to completeley abandon constructor dependency injection!

This is not an alternative for us because of the testing pains and because it would mean to tightly couple our unit tests to Angular's TestBed (and because we would need 60 test shards then instead of 30 because I'd expect overall test runtime to double).

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Breaking Change Would introduce errors in existing code Domain: JS Emit The issue relates to the emission of JavaScript Rescheduled This issue was previously scheduled to an earlier milestone
Projects
None yet
Development

No branches or pull requests

7 participants