Skip to content

Downlevel this.#x &&= v / ||= / ??= on a private accessor always calls the setter #65333

Open

Description

@trackoor

🔎 Search Terms

private logical assignment, &&= private, ||= private, ??= private, __classPrivateFieldSet short-circuit, es2021 private accessor

🕗 Version & Regression Information

Same behavior in 5.9.3, 7.0.2 and 7.1.0-dev.20261105.1 (typescript@next). Only affects targets that lower #private (es2021 and below); --target es2022 is correct.

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=next&target=8#code/G4QwTgBANg9g5gLggZwC5gJYDs4G0C6EAvBAQNwBQAxlCMshAMIQDeFEEcApqhAMQAzABQBKVtHgA6AA4BXZAAshAIm6plIshDA9ZYLBAAMWgL7sUPfsOBIssgLYAjLmDEsJcGfKXLkl5RAA1BDAmhBmHGCyWKLiqAoYyJKCEABkqSQAjFo6qHoGsJ4AVjDYoqYUZlQwWMgwUFyShUJYXADuTKKSUTEimhRAA

💻 Code

var log: string[] = [];
class C {
  get #f() { log.push("get"); return 0; }
  set #f(v: number) { log.push("set " + v); }
  run() { this.#f &&= 1; return log.join(); }
}
console.log(new C().run());

🙁 Actual behavior

tsc --strict --target es2021 --outDir out c.ts && node out/c.js prints get,set 0.

The emit writes unconditionally, with the short-circuited value:

__classPrivateFieldSet(this, _C_instances, __classPrivateFieldGet(this, _C_instances, "a", _C_f_get) && 1, "a", _C_f_set);

??= (getter returning 0, so not nullish) and ||= (getter returning 1) behave the same way: the setter runs with the current value.

🙂 Expected behavior

get, as with --target es2022 or running the source directly. For &&= the spec returns lval without PutValue when it is falsy (https://tc39.es/ecma262/#sec-assignment-operators-runtime-semantics-evaluation); ||= and ??= are analogous. Public members are already lowered correctly (o.p && (o.p = v)).

Additional information about the issue

Observable whenever the setter has side effects or there is no setter. With a private method the downlevel code throws TypeError: Private method is not writable even when no write should happen (this.#m ||= 1), though that case already gets TS2803.

The fix is probably in the logical-assignment branch for private identifiers in transformers/classFields.ts: emit get && set(...) (resp. ||, and a nullish check for ??=) instead of set(get && v). Closest existing issues: #65287 (different defect, closed) and #39172 (public members, 2020).

This was found with an AI-assisted differential testing tool; the repro and outputs above were re-run on the listed versions.

Activity

  1. RyanCavanaugh commented on Nov 5, 2026

    @RyanCavanaugh
    Member

    trackoor meta-discussion: we're looking at AI-generated bug reports and trying to figure out why some are really good (this one) and some are really bad (most of the others out there). Can you go into details on what prompt, model, and approach you're using?

  2. morluto commented on Nov 5, 2026

    @morluto

    nice.

  3. trackoor commented on Nov 6, 2026

    @trackoor
    Author

    trackoor meta-discussion: we're looking at AI-generated bug reports and trying to figure out why some are really good (this one) and some are really bad (most of the others out there). Can you go into details on what prompt, model, and approach you're using?

    Thanks so much for the kind words! Ryan Cavanaugh (@RyanCavanaugh)

    We‘re actually working on a formal verification project at UCSB PLSE lab. We've built an end-to-end formally verified TypeScript compiler and runtime, covering over 70% of the language features using a new compiler verification framework that's both agent-friendly and expressive enough to support aggressive compiler optimizations.

    The idea is pretty simple:
    (1) We use our verified implementation as the ground truth (spec). We also have an internal LLM post-trained for formal verification tasks to help speed up the verification process and reduce its cost.
    (2) Run differential fuzzing against existing implementations (it takes ~30 min to produce the bugs in this report),
    (3) And then, use AI to triage and report the bugs.

    I think one thing we've learned is that with LLMs getting so good at reproducing PoCs. The really hard part is getting the specs and invariants right. Without those, it's very difficult to tell whether something is actually a bug and LLMs often hallucinate, or reporting on trivial petty matters that don’t really matter, etc...

    And that's also why I think formally verified implementations are so useful here. In some sense, they're the most complete executable specs you can have. Most of the bugs we've found recently weren't covered by any static test suite (e.g. test262).

    Happy to share more if needed!

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

Metadata

Metadata

Labels

BugA bug in TypeScript

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions