Repository navigation
Add flow path for a falsy, non-nullish left operand of ?? and ??= - #65409
Ryan Cavanaugh (RyanCavanaugh) with Copilot wants to merge 4 commits into
Conversation
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
?? and ??=
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved control-flow errors affect nested optional chains and parenthesized literals, and missing reference baselines cause the new regression test to fail.
3 open issues
What changed in this PR
This PR addresses #65402 by adding control-flow paths for falsy, non-nullish left operands of ?? and ??=, correcting false unreachable-code diagnostics and branch narrowing in the native TypeScript compiler.
Changes:
- Adds a flow flag and checker support for falsy, non-nullish conditions.
- Updates binder routing for nullish expressions, nested chains, and optional-chain roots.
- Adds regression cases covering reachability, narrowing, literals, and logical operands.
| File | Description |
|---|---|
tsc/testdata/tests/cases/compiler/controlFlowNullishCoalescingFalsyLeft.ts |
Adds regression cases for nullish control flow. |
tsc/internal/checker/flow.go |
Narrows the new flow condition to non-nullish, then falsy. |
tsc/internal/binder/binder.go |
Adds and routes falsy, non-nullish flow paths. |
tsc/internal/ast/flow.go |
Defines FlowFlagsNonNullish. |
Review effort: Balanced
| // @strict: true | ||
| // @allowUnreachableCode: false | ||
| // @target: esnext |
| } | ||
| return b.unreachableFlow | ||
| } | ||
| if flags&ast.FlowFlagsNonNullish != 0 && (expression.Kind == ast.KindTrueKeyword || expression.Kind == ast.KindNullKeyword) { |
|
Copilot 🚲 |
Could you clarify what you’d like me to do? Does 🚲 mean to address the earlier review findings? |
|
Copilot do another "cycle" of work on this - you have high-priority comments, and failing CI! |
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com>
Added the missing compiler baselines and regression cases in 1ff6f2b; fixed parenthesized literal reachability and nested optional-chain side-effect routing in f89b8bb. |


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:Changes
ast/flow.go: adds theFlowFlagsNonNullishflag. Combined withFlowFlagsFalseCondition, it means the left operand of??/??=is falsy but not nullish.checker/flow.go:getTypeAtFlowConditionnarrows a condition with this flag to non-nullish first, then to falsy.binder.go: the newbindNullishCoalescingLeftadds a path from the left operand to a falsy target:FalseCondition|NonNullishcondition 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.a ?? b ?? c: two new binder fields,currentNullishTargetandcurrentFalsyTarget, track the enclosing??. A??whose false target is the enclosing??'s pre-right label sends its falsy path to the outer falsy target. This keepscfrom being narrowed as ifacould be"".(a ?? b)?.c: a falsy, non-nullishacontinues the chain rather than short-circuiting it.createFlowCondition: atrueornullleft operand with the new flag is unreachable. This keeps the existing TS7027 fortrue ?? …andnull ?? ….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 whatwhile (a)already does. As a result,if (a ?? b) {} else { const r: false = b; }witha: object | undefinednow reports TS2322, asif (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.??is falsy but not nullish #65402