Skip to content

Fix downlevel emit for private names in for-in/for-of assignment targets - #65427

Open
Thanh Nguyen (RealThanhNguyxn) wants to merge 1 commit into
microsoft:mainfrom
RealThanhNguyxn:fix-65413-private-for-in-of-target
Open

Thanh Nguyen (RealThanhNguyxn) wants to merge 1 commit into
microsoft:mainfrom
RealThanhNguyxn:fix-65413-private-for-in-of-target

Conversation

@RealThanhNguyxn

Copy link
Copy Markdown

Fixes #65413

When downleveling for (this.#x of ...) / for (this.#x in ...), the private name was lowered to __classPrivateFieldGet(...) directly in the loop initializer. The get helper returns a value rather than a Reference, so the emitted code fails at runtime with ReferenceError: Invalid left-hand side in for-loop. The same happened for destructuring targets such as for ([this.#x] of ...), which emitted a SyntaxError instead.

This changes the class fields transformer to bind the iteration value to a temporary and perform the (already lowered) assignment at the start of the loop body when the initializer contains a private name that needs transforming. This keeps the assignment target evaluation per iteration, which matters for side-effecting receivers (e.g. for (getReceiver().#x of ...) must call getReceiver() for every value). The original body is kept in its own block so that bindings declared in it cannot shadow identifiers used by the assignment target.

The existing destructuring wrapper (wrapPrivateIdentifierForDestructuringTarget) is not reused here because it pushes the receiver assignment into pendingExpressions, which is hoisted out of the loop.

The fix covers private fields, private accessors, static private members, for...in, for...of, for await...of, and destructuring assignment targets.

Tests

  • Added conformance test privateNameForInOfTarget.ts with baselines.
  • go test ./internal/testrunner
  • go test ./internal/compiler ./internal/transformers/...
  • Runtime checks on Node for targets ES2015-ES2021, including getReceiver().#x calling the receiver twice and body bindings that would otherwise shadow the target receiver.

The assignment target of a for-in/for-of statement is evaluated on each
iteration, so a private name cannot be lowered to a __classPrivateFieldGet
call in the initializer; the helper returns a value, not a Reference.

When the initializer contains a private name that must be transformed, bind
the iteration value to a temporary and perform the (transformed) assignment
at the start of the loop body. The original body is kept in its own block so
that bindings declared in it cannot shadow identifiers used by the target.

This also fixes destructuring assignment targets such as
`for ([this.#x] of ...)`, which previously emitted the get helper as an
element of the pattern.
Copilot AI balanced review requested due to automatic review settings November 10, 2026 11:15
@github-project-automation github-project-automation Bot moved this to Not started in PR Backlog Nov 10, 2026
@typescript-automation typescript-automation Bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Nov 10, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Destructuring private-target lowering can evaluate receivers too early and update the wrong object, and for-await private-target regression coverage is missing across compilation targets.

2 open issues
What changed in this PR

Addresses #65413 by lowering private-name loop assignment targets into assignments at the start of each iteration, rather than emitting invalid helper calls as loop targets.

Changes:

  • Introduces temporary loop bindings while preserving the original body’s scope.
  • Adds conformance cases for fields, accessors, static members, destructuring, receiver side effects, and shadowing.
  • Adds JavaScript, type, and symbol baselines.
File Description
tsc/​testdata/​tests/​cases/​conformance/​classes/​members/​privateNames/​privateNameForInOfTarget.ts Adds private-name loop regression cases.
tsc/​testdata/​baselines/​reference/​conformance/​privateNameForInOfTarget.types Records expected types.
tsc/​testdata/​baselines/​reference/​conformance/​privateNameForInOfTarget.symbols Records expected symbol bindings.
tsc/​testdata/​baselines/​reference/​conformance/​privateNameForInOfTarget.js Records expected downlevel JavaScript.
tsc/​internal/​transformers/​estransforms/​classfields.go Rewrites affected loop targets into body assignments.

Review effort: Balanced

)
declarationList.Loc = statement.Initializer.Loc

assignmentExpression := f.NewAssignmentExpression(statement.Initializer, temp)

return f.UpdateForInOrOfStatement(
statement,
statement.AwaitModifier,

This branch has not been deployed

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

Labels

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

Projects

Status: Not started

Development

Successfully merging this pull request may close these issues.

Downlevel for (this.#x of ...) / for (this.#x in ...) emits __classPrivateFieldGet(...) as the loop target

3 participants