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

Incorrect output for #private with nested class in ESNext #41788

Closed
DanielRosenwasser opened this issue Dec 2, 2020 · 8 comments · Fixed by #42663
Closed

Incorrect output for #private with nested class in ESNext #41788

DanielRosenwasser opened this issue Dec 2, 2020 · 8 comments · Fixed by #42663
Labels
Bug A bug in TypeScript Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Fix Available A PR has been opened for this issue Help Wanted You can do this Rescheduled This issue was previously scheduled to an earlier milestone

Comments

@DanielRosenwasser
Copy link
Member

Found from #41773 (comment)

// @target: esnext

class A {
    #str = 'pr Str'
    constructor() {
        new A.B(this)
    }
    private static B = class {
        constructor(private a: A) {
            console.log(this.a.#str)
        }
    }
}
test: {
    new A
}

Currently, this is emitted as

"use strict";
class A {
    constructor() {
        this.#str = 'pr Str';
        new A.B(this);
    }
    #str;
}
A.B = class {
    constructor(a) {
        this.a = a;
        console.log(this.a.#str);
    }
};
test: {
    new A;
}

However, #str has been orphaned and this code will have errors.

CC @robpalme

@RyanCavanaugh RyanCavanaugh added the Bug A bug in TypeScript label Dec 3, 2020
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 4.2.0 milestone Dec 3, 2020
@DanielRosenwasser DanielRosenwasser added Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Help Wanted You can do this labels Dec 4, 2020
@robpalme
Copy link

robpalme commented Dec 5, 2020

Thanks for highlighting this. I played around and reduced it to a slightly simpler case in the playground.

// @target: esnext

class A {
    #x : undefined;
    static B = class {
        constructor() {
            new A().#x;
        }
    }
}

...produces the following incorrect ESnext emit...

class A {
    #x;
}
A.B = class {
    constructor() {
        new A().#x;
    }
};

...whereas desired ESnext emit should retain the static field initialization inside the class...

class A {
    #x;
    static B = class {
        constructor() {
            new A().#x;
        }
    }
}

Making the static field assignment work without useDefineForClassFields might be possible if we sneakily export a getter.

class A {
    #x;
    static __bornToDie = t=>t.#x;
}
var __get_A_X = A.__bornToDie;
delete A.__bornToDie;
A.B = class {
    constructor() {
        __get_A_X(new A());
    }
};

But this feels like a lot of work for the matter at hand. Maybe the simplest solution would be to make target:ESnext imply useDefineForClassFields:true to side-step the problem entirely. Alternatively, an easy/quick solution to achieve correctness would be to degrade the target:ESnext && useDefineForClassFields:false case to use down-levelling.

Until this is fixed, users of target:ESNext can workaround this by enabling useDefineForClassFields

@a-tarasyuk
Copy link
Contributor

@DanielRosenwasser I suppose static methods/properties should be transformed because the proposal is at Stage 3. Am I right?

@a-tarasyuk
Copy link
Contributor

Why I asked this question, maybe it doesn't make sense, https://github.com/tc39/proposal-class-fields is at Stage 3 too, and TS emits Private fields without polyfills. Example

class A {
    #x = undefined;
}


// .js
"use strict";
class A {
    constructor() {
        this.#x = undefined;
    }
    #x;
}

However, TS downgrades static methods/properties. Example

class Foo {
  static x = 1;
}

// .js
"use strict";
class Foo {
}
Foo.x = 1;

@sandersn
Copy link
Member

I like @robpalme's idea of ESNext implying useDefineForClassFields; the emit is simpler and spec-standard class properties are widely enough supported that people overusing target:esnext [1] are unlikely to run into problems.

For the explicit combination of (1) target:esnext (2) useDefineForClassFields: false (3) nested static class with private reference, we should add an error that says "you can't do this, change useDefineForClassFields: true, or target an earlier version of ES."

[1] for example, somebody who just wants to use every feature in JS without the compiler doing any downlevelling, but who hasn't thought too much about which browser will.

@dragomirtitian
Copy link
Contributor

@sandersn I would like to pick this up if that's ok. Just to clear on the statement of work:

  1. If target:esnext,then useDefineForClassFields: true will now be the default.
  2. If target:esnext, useDefineForClassFields: true we emit correct code so no further action is needed (ex)
  3. If target:esnext, useDefineForClassFields: false and we encounter a usage of a #private field in a static member we emit an error like "You can't do this, change useDefineForClassFields: true, or target an earlier version of ES."

@typescript-bot typescript-bot added the Fix Available A PR has been opened for this issue label Feb 5, 2021
dragomirtitian added a commit to bloomberg/TypeScript that referenced this issue Feb 25, 2021
…-41788-incorrect-output-for-esprivate-with-nested-class-in-esnext
@RyanCavanaugh RyanCavanaugh added the Rescheduled This issue was previously scheduled to an earlier milestone label Mar 4, 2021
dragomirtitian added a commit to bloomberg/TypeScript that referenced this issue Mar 11, 2021
…-41788-incorrect-output-for-esprivate-with-nested-class-in-esnext
@rbuckton
Copy link
Member

rbuckton commented Mar 18, 2021

I would strongly discourage using delete, as that is a performance cliff in V8 and other engines and implementers have strongly advised not to do this.

One approach I've seen is to leverage a computed property name, if possible:

var B_1;
class A {
    #x;
    static [(B_1 = class {
        constructor() {
            new A().#x;
        }
    }, "B"]() {}
}
A.B = B_1;

This, of course, doesn't work if the class has no non-private members, in which case we might have to add an unused member, as you propose, but don't delete it.

var <temp variables>;
class A {
  #x;
  // NOTE: __tsfieldinit__ would be an optimistic unique name
  // NOTE: __tsfieldinit__ is a placeholder. It won't actually be evaluated.
  static [(<inlined expressions>, "__tsfieldinit__")]() {} 
}
<static assignments>
A.__tsfieldinit__ = void 0; // mark undefined, but otherwise leave as is. `delete` is terrible for performance...

NOTE: we could possibly abuse name in this case..., though I'm not sure that's much better:

var <temp variables>;
class A {
  #x;
  static [(<inlined expressions>, "name")]() {}
}
Object.defineProperty(A, "name", { value: "A", writable: false }); // fix up A.name
<static assignments>;

@rbuckton
Copy link
Member

I like @robpalme's idea of ESNext implying useDefineForClassFields; the emit is simpler and spec-standard class properties are widely enough supported that people overusing target:esnext [1] are unlikely to run into problems.

The downside of this is that it could be considered silent breaking change (if you previously used --target:esnext on its own, your class field emit changes without notification, which can lead to subtle breaks). If we were going to make a change, I'd suggest that --target esnext should make you explicitly define your choice for --useDefineForClassFields if you use class fields in your code (assuming you haven't set --noEmit), at least for a few releases (or a future TS 5.x) to make people aware of the change.

@rbuckton
Copy link
Member

We do have other cases where we downlevel things like destructuring to the ES5 form even when targeting ES2015, particularly in the module transforms, which is why destructuring.ts is separate from the es2015.ts transform. We could consider forcing the downlevel private field emit when we encounter the combination.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Effort: Moderate Requires experience with the TypeScript codebase, but feasible. Harder than "Effort: Casual". Fix Available A PR has been opened for this issue Help Wanted You can do this Rescheduled This issue was previously scheduled to an earlier milestone
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants