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

Emit defineProperty calls before param prop assignments #34987

Merged
merged 4 commits into from Nov 22, 2019

Conversation

sandersn
Copy link
Member

@sandersn sandersn commented Nov 7, 2019

Note that I restricted this to when --useDefineForClassFields is true. Nothing changes when it's off. I think this is the correct fix for a patch release.

However, in principal there's nothing wrong with moving parameter property initialisation after property declaration initialisation. It would be Extremely Bad and Wrong to rely on this working:

class C {
  p = this.q // q is already initialised here!
  constructor(public q: number) { }
}

But today it emits the below code, and probably somebody relies on it without knowing:

class C {
    constructor(q) {
        this.q = q;
        this.p = p;
    }
}

Fixes #34942

Note that I restricted this to --useDefineForClassFields is true.
Nothing changes when it's off. I think this is the correct fix for a
patch release.

However, in principal there's nothing wrong with moving parameter
property initialisation after property declaration initialisation. It
would be Extremely Bad and Wrong to rely on this working:

```ts
class C {
  p = this.q // what is q?
  constructor(public q: number) { }
}
```

But today it does, and probably somebody relies on it without knowing.
@ajafff
Copy link
Contributor

ajafff commented Nov 8, 2019

probably somebody relies on it

👋 a lot of code in my current project relies on this behavior.
IIRC the order is documented somewhere so changing it would be a huge breaking change.

@sandersn
Copy link
Member Author

sandersn commented Nov 8, 2019

@ajafff could it be in the spec? I'll have to take a look. If that's the case the long-term fix would need to be an interleaving of parameter property defineProperty/assignments, all happening before the defineProperty calls for other property declarations.

@sandersn
Copy link
Member Author

sandersn commented Nov 8, 2019

Or, after discussing with @RyanCavanaugh, removing the assignments and using the parameter as the initial value of the defineProperty call.

@Jessidhia
Copy link

Jessidhia commented Nov 11, 2019

That'd work as a quick fix for now, but I'd like to raise an issue with the semantics of parameter properties to begin with. The way it's specified for TypeScript is insufficiently clear, and the most direct interpretation of it actually means it should, in your first class C snippet, raise a "use before initialization" error in p = this.q with --strictNullChecks.

I wrote this in a bug report that got closed as a duplicate:

This depends on how the parameter property is going to be interpreted. If it's still going to be interpreted as the same as writing an assignment in the constructor's body, then it needs to be reordered to happen after all of the Object.defineProperty initializers (as they technically run before super() returns, or before the constructor block begins evaluating for non-subclassing classes), and bar's initializer should detect this.foo as used before initialization.

If they're going to be "as powerful as" actual class fields, then constructor parameter properties must be hoisted higher than any actual class field initializers. Even with this latter interpretation, which is almost what happened, the this.foo = foo assignment is incorrect and should be done either immediately after the Object.defineProperty for foo runs, or the Object.defineProperty should use the constructor parameter as value initializer.

But for the spec issue itself and how to handle it going forward, should I open a new issue? This is important for @babel/transform-typescript purposes as well, and if you opt for the "backwards compatible" way we run into a conflict with the ecma262 initialization order.

@sandersn
Copy link
Member Author

@Jessidhia I think the right thing is to have Object.defineProperty use the constructor parameter as the value initializer. I think this resolves the conflict with ecma262 initialisation order.

Although this is complex enough that perhaps we should have a new issue to find out if we need changes to accomodate the class fields proposal.

@sandersn
Copy link
Member Author

@rbuckton mind taking a look at this? This is the fix we talked about yesterday

@Jessidhia We decided that we won't make changes to parameter properties unless the TC39 committee considers a similar feature. The feature as specified is kind of weird and fits badly with the ECMAScript spec, but changing it will certainly break people. I don't think it's worthwhile to improve it unless we're forced to.

@ajafff
Copy link
Contributor

ajafff commented Nov 15, 2019

We decided that we won't make changes to parameter properties unless the TC39 committee considers a similar feature. The feature as specified is kind of weird and fits badly with the ECMAScript spec, but changing it will certainly break people. I don't think it's worthwhile to improve it unless we're forced to.

Does that mean you always need to use Object.defineProperty for classes with parameter properties even with useDefineForClassFields: true, target: esnext? As far as I understand the spec, TypeScript is not able to place the parameter property initialisation before initialisation of (native) class fields.

@DanielRosenwasser
Copy link
Member

🔔 reviewers can we get some eyes on this so we can get 3.7.3 out sooner?

@DanielRosenwasser
Copy link
Member

@typescript-bot cherry-pick this to release-3.7

typescript-bot pushed a commit to typescript-bot/TypeScript that referenced this pull request Nov 20, 2019
Component commits:
5810765 Emit defineProperty calls before param prop assignments
Note that I restricted this to --useDefineForClassFields is true.
Nothing changes when it's off. I think this is the correct fix for a
patch release.

However, in principal there's nothing wrong with moving parameter
property initialisation after property declaration initialisation. It
would be Extremely Bad and Wrong to rely on this working:

```ts
class C {
  p = this.q // what is q?
  constructor(public q: number) { }
}
```

But today it does, and probably somebody relies on it without knowing.

ec79590 Put parameter property initialiser into defineProperty's value
@typescript-bot
Copy link
Collaborator

Hey @DanielRosenwasser, I've opened #35242 for you.

@@ -0,0 +1,20 @@
// @target: esnext
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just merge this with definePropertyES5.ts (and rename it?) and use this:

// @target: es5, esnext

@sandersn sandersn merged commit 369900b into master Nov 22, 2019
@sandersn sandersn deleted the fix-defineProperty-parameter-property-emit branch November 22, 2019 23:37
@sandersn
Copy link
Member Author

@typescript-bot cherry-pick this into release-3.7

typescript-bot pushed a commit to typescript-bot/TypeScript that referenced this pull request Nov 22, 2019
Component commits:
5810765 Emit defineProperty calls before param prop assignments
Note that I restricted this to --useDefineForClassFields is true.
Nothing changes when it's off. I think this is the correct fix for a
patch release.

However, in principal there's nothing wrong with moving parameter
property initialisation after property declaration initialisation. It
would be Extremely Bad and Wrong to rely on this working:

```ts
class C {
  p = this.q // what is q?
  constructor(public q: number) { }
}
```

But today it does, and probably somebody relies on it without knowing.

ec79590 Put parameter property initialiser into defineProperty's value

be86355 Merge branch 'master' into fix-defineProperty-parameter-property-emit

8ff59b9 Combine ES5/ESNext into one test
@typescript-bot
Copy link
Collaborator

Hey @sandersn, I've opened #35303 for you.

sandersn pushed a commit that referenced this pull request Nov 23, 2019
Component commits:
5810765 Emit defineProperty calls before param prop assignments
Note that I restricted this to --useDefineForClassFields is true.
Nothing changes when it's off. I think this is the correct fix for a
patch release.

However, in principal there's nothing wrong with moving parameter
property initialisation after property declaration initialisation. It
would be Extremely Bad and Wrong to rely on this working:

```ts
class C {
  p = this.q // what is q?
  constructor(public q: number) { }
}
```

But today it does, and probably somebody relies on it without knowing.

ec79590 Put parameter property initialiser into defineProperty's value

be86355 Merge branch 'master' into fix-defineProperty-parameter-property-emit

8ff59b9 Combine ES5/ESNext into one test
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
6 participants