Skip to content

Add flow path for a falsy, non-nullish left operand of ?? and ??= - #65409

Open
Ryan Cavanaugh (RyanCavanaugh) with Copilot wants to merge 4 commits into
mainfrom
copilot/fix-ts7027-narrowing-issue
Open

Ryan Cavanaugh (RyanCavanaugh) with Copilot wants to merge 4 commits into
mainfrom
copilot/fix-ts7027-narrowing-issue

Conversation

Copilot AI commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

The binder treats the left operand of ?? and ??= like the left operand of ||. A non-nullish left operand goes to the true branch and a nullish one goes to the right operand. A left operand that is falsy but not nullish (false, 0, "") makes the whole expression falsy without running the right operand, but the flow graph has no path for it. This causes false TS7027 errors and wrong narrowing in the false branch:

declare function c(): boolean;
while (c() ?? (c() || true)) {}
c(); // before: TS7027 (false positive); after: reachable

function f(a: "" | undefined, b: boolean) {
  if (a ?? b) {} else {
    const u: undefined = a; // before: no error (a narrowed to undefined); after: TS2322 ("" | undefined)
  }
}

Changes

  • ast/flow.go: adds the FlowFlagsNonNullish flag. Combined with FlowFlagsFalseCondition, it means the left operand of ??/??= is falsy but not nullish.
  • checker/flow.go: getTypeAtFlowCondition narrows a condition with this flag to non-nullish first, then to falsy.
  • binder.go: the new bindNullishCoalescingLeft adds a path from the left operand to a falsy target:
    • Simple left operand: adds a FalseCondition|NonNullish condition on the operand. It is skipped when the falsy target equals the true target, as in a ?? whose value is not used as a condition. There, the existing true condition already includes falsy non-nullish values.
    • &&, ||, ?? or ! as the left operand: the label before the right operand also flows to the falsy target. This over-approximates, since some of those paths are nullish, but it never drops a real path.
    • Chains like a ?? b ?? c: two new binder fields, currentNullishTarget and currentFalsyTarget, track the enclosing ??. A ?? whose false target is the enclosing ??'s pre-right label sends its falsy path to the outer falsy target. This keeps c from being narrowed as if a could be "".
    • (a ?? b)?.c: a falsy, non-nullish a continues the chain rather than short-circuiting it.
    • createFlowCondition: a true or null left operand with the new flag is unreachable. This keeps the existing TS7027 for true ?? … and null ?? ….

Behavior change

The binder does not use types, so a left operand that can never be falsy (e.g. a: object) also gets the new path. This matches what while (a) already does. As a result, if (a ?? b) {} else { const r: false = b; } with a: object | undefined now reports TS2322, as if (a && b) already does.

New test: tsc/testdata/tests/cases/compiler/controlFlowNullishCoalescingFalsyLeft.ts. It covers literals, ??=, chains, logical left operands, nesting inside ||, and optional-chain roots with no new false positives.

Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix false TS7027 and wrong narrowing with falsy left operand Add flow path for a falsy, non-nullish left operand of ?? and ??= Nov 9, 2026
@RyanCavanaugh
Ryan Cavanaugh (RyanCavanaugh) marked this pull request as ready for review November 9, 2026 19:56

This comment was marked as outdated.

@RyanCavanaugh

Copy link
Copy Markdown
Member

Copilot 🚲

Copilot AI commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

Copilot 🚲

Could you clarify what you’d like me to do? Does 🚲 mean to address the earlier review findings?

@RyanCavanaugh

Copy link
Copy Markdown
Member

Copilot do another "cycle" of work on this - you have high-priority comments, and failing CI!

Copilot AI and others added 2 commits November 9, 2026 22:05
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>

Copilot AI commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

Copilot do another "cycle" of work on this - you have high-priority comments, and failing CI!

Added the missing compiler baselines and regression cases in 1ff6f2b; fixed parenthesized literal reachability and nested optional-chain side-effect routing in f89b8bb. npx hereby validate completed successfully, including the full test suite, lint, and formatting. Secret scans passed; CodeQL analysis was skipped by the tool because its databases exceeded the size limit.

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

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.

False TS7027 and wrong narrowing when the left operand of ?? is falsy but not nullish

3 participants