Skip to content

Incorrect operator precedence when using private fields with nullish coalescing assignment for targets <= es2021 #61109

Open
@pavadeli

Description

@pavadeli

🔎 Search Terms

"nullish coalescing assignment", "private properties", "private fields", "target es2021", "operator precedence"

🕗 Version & Regression Information

  • This changed between versions 3.9.7 and 4.0.5

(with the introduction of nullish coalescing assignment)

⏯ Playground Link

https://www.typescriptlang.org/play/?noUnusedLocals=true&noUnusedParameters=true&target=8&jsx=0&noFallthroughCasesInSwitch=true&useUnknownInCatchVariables=true&ts=5.8.0-dev.20250204&ssl=6&ssc=1&pln=9&pc=1#code/MYGwhgzhAEDCIwN4ChpugBwK4CMQEtgAFAJwHsMAuaAOywFscBTE6AH2ixoBMmAzfDSbcA3KnQBiDCXwA3MABcmpCtTqMW7Tj36DhY8WhpkVeJvQAUASmgp096AHpH0ACoALfDADuZEgGsYSGgmAA8MJmAlbmolEhowEgBPEPCSJih8MhpoMB5oBXcmWiwQAgh3aGAyMBAM4EEAc0MHZ1zMxpp6JhoFXPSQ+RAsRWFoGUb3BQBaBTJpur4FABpoCDICooKWBOTU6QyILJyvFvs2xvIsCO5c-K9obJAUpiGR6OhvIpyAA0KvAB02DwhBUGB+0AedDKXncALO6H+ECBuAIxHIGGgAH4sQBeaB8WoQYpY2ivFgeLzWaDUACMACYAMxiBzoNqUnx+QKrZjAMBYYmfYoC4oYRI9QoZDKUBFoJEAqQyeRKMHYvHQCyEhAksmyCmeCDU6j0gAMVhZ6AAvsgWtIyGZLDY7KynC4OQSwPgEDzIvzBZLoD8cbiIQ8FCQ8hBwB85rlAziIV9CJV3MEABIASQA4mmAKIAJVlrsw6WAwh6Zc2eU2xTiuxeaUOx3hLs2gMVclGquDHu12N1+qpNmNJotaGt1uQ1Ro6zqANATES1gM04gfVY+KE3jgCDEJABxlMdUde4PJnIDuXNtXZDnIDIjQsAHJjCX7ce1htCSQn+abfu7UvP8p2yWcmABe9HxfDZAI-RRcjKX8DD4LgomOAcSA5aw1HJVhnTldxyG3LdoFzEhyBIZ93QqMhSludJamecZFzKFIhD1VhUwwCIaAAQiQ5BLSAA

(BTW: run it to see the problem)

💻 Code

class Cls {
    publicProp: number | undefined;
    #privateProp: number | undefined;

    noProblem() {
        // This works as expected: ternary expression and the nullish coalescing
        // assignment are evaluated right-to-left, so the ternary expression is
        // grouped and is only evaluated when `this.publicProp` is nullish.
        this.publicProp ??= false ? neverThis() : 123;
        // This works, because we use parentheses:
        this.#privateProp ??= (false ? neverThis() : 20);
    }

    problem() {
        // This fails, because the `??=` is translated to a `??` which has HIGHER
        // precedence than the ternary expression.
        this.#privateProp ??= false ? neverThis() : 20;
    }
}

console.clear();

const r = new Cls;
r.noProblem();
r.noProblem();

console.log('no problem so far');

r.problem();

console.log('no problem at all');

function neverThis(): never {
    throw new Error('This should really really never happen!');
}

🙁 Actual behavior

The statement this.#privateProp ??= false ? neverThis() : 20; is translated into:

__classPrivateFieldSet(this, _Cls_privateProp, __classPrivateFieldGet(this, _Cls_privateProp, "f") ?? false ? neverThis() : 20, "f");

The relevant piece here is:

__classPrivateFieldGet(this, _Cls_privateProp, "f") ?? false ? neverThis() : 20

which is equivalent to:

(__classPrivateFieldGet(this, _Cls_privateProp, "f") ?? false) ? neverThis() : 20

But it should have been:

__classPrivateFieldGet(this, _Cls_privateProp, "f") ?? (false ? neverThis() : 20)

🙂 Expected behavior

Expected correct operator precedence in transpiled code.

Additional information about the issue

This is no issue on targets greater than es2021, because of the native support of private fields.

Metadata

Metadata

Assignees

Labels

BugA bug in TypeScriptFix AvailableA PR has been opened for this issue

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions