Skip to content

[WIP] Fix downlevel emit of logical assignment operators - #65295

Closed
Ryan Cavanaugh (RyanCavanaugh) with Copilot wants to merge 1 commit into
mainfrom
copilot/fix-logical-assignment-evaluated-twice
Closed

Ryan Cavanaugh (RyanCavanaugh) with Copilot wants to merge 1 commit into
mainfrom
copilot/fix-logical-assignment-evaluated-twice

Conversation

Copilot AI commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Thanks for asking me to work on this. I will get started on it and keep this PR's description up to date as I form a plan and make progress.


This section details on the original issue you should resolve

<issue_title>Downlevel emit of ||=, &&=, ??= and **= evaluates a member target twice when it has !, as or satisfies</issue_title>
<issue_description>> This issue was drafted with the help of LLMs (Claude and Codex); I reviewed it and verified the reproductions.

Search Terms

logical assignment evaluated twice, ||= non-null assertion downlevel, ??= type assertion side effect, **= as Math.pow evaluated twice, PartiallyEmittedExpression logical assignment, ES2020 target logical assignment side effects

Version & Regression Information

  • This is the behavior in every version I tried (4.0.2, 4.9.5, 5.9.3, 6.0.3, 7.0.2 and 7.1.0-dev.20261103.1), and I reviewed the FAQ for entries about type assertions and downlevel emit

Code

let key = 0;
const obj: Record<number, number> = {};
obj[++key]! ||= 1;
console.log(key, JSON.stringify(obj));

Compiled with tsc --target es2020 input.ts, which reports no errors.

Actual behavior

"use strict";
let key = 0;
const obj = {};
obj[++key] || (obj[++key] = 1);
console.log(key, JSON.stringify(obj));

node input.js prints 2 {"2":1}: ++key runs again in the assignment branch, and the value is written to a different property than the one that was read.

The same happens with &&= and ??= whenever the assignment branch runs, with as, satisfies and angle-bracket assertions around the target, and with an effectful receiver: getObject().x! ||= 1 calls getObject() twice. **= with --target es2015 has the same problem: obj[++key]! **= 2 is emitted as obj[++key] = Math.pow(obj[++key], 2).

Expected behavior

The ! is type-only and should not change evaluation: ++key should run once, and the output should print 1 {"1":1}, as node input.ts does with Node's type stripping. Without the !, the same file is emitted as obj[_a = ++key] || (obj[_a] = 1), and with --target es2021 the operator is kept as is; both print 1 {"1":1}.

Additional information about the issue

Steps, in an empty directory with Node.js 26.13.0:

npm install --save-exact typescript@7.0.2
npx tsc --target es2020 input.ts
node input.ts
node input.js

The type eraser replaces !, as, satisfies and angle-bracket assertions with a PartiallyEmittedExpression (typeeraser.go#L263-L267). The logical assignment transform skips only parentheses before testing for an access expression (logicalassignment.go#L37-L41), so the test fails and the same left node becomes both the read and the write target (L96-L107). The exponentiation transform skips nothing before its element and property access tests and falls back to target = left and value = left (exponentiation.go#L37-L69). In the 6.0.3 JavaScript build, skipping partially emitted expressions together with parentheses at these two places (skipOuterExpressions(left, OuterExpressionKinds.Parentheses | OuterExpressionKinds.PartiallyEmittedExpressions)) makes every case above evaluate the target once; I have not built the Go compiler with the same change.

#39172 fixed the same duplication for plain element access targets in #39174, and that case still works. #44639 was the same kind of problem in optional chaining: a type assertion around the callee made the call lose this.
</issue_description>

Comments on the Issue (you are Copilot in this section)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Downlevel emit of ||=, &&=, ??= and **= evaluates a member target twice when it has !, as or satisfies

2 participants