Repository navigation
Type is not referred correctly in the while loopΒ #59715
Copy link
Copy link
Open
Labels
BugA bug in TypeScriptA bug in TypeScriptDomain: check: Control FlowThe issue relates to control flow analysisThe issue relates to control flow analysisHelp WantedYou can do thisYou can do this
Milestone
Description
π Search Terms
incorrect type while loop
π Version & Regression Information
Worked correctly in version 4.2.3.
It doesn't work since 4.3.5.
β― Playground Link
π» Code
class Child extends HTMLElement {
get parent() {
const { parentElement } = this;
if (!(parentElement instanceof Parent)) {
return null;
}
return parentElement;
}
}
class Parent extends HTMLElement {
get parent() {
const { parentElement } = this;
if (!(parentElement instanceof GrandParent)) {
return null;
}
return parentElement;
}
}
class GrandParent extends HTMLElement {
get parent() {
const { parentElement } = this;
if (!(parentElement instanceof GreatGrandParent)) {
return null;
}
return parentElement;
}
}
class GreatGrandParent extends HTMLElement {
get parent() {
return null;
}
}
// irrelevant code
customElements.define("c-child", Child);
customElements.define("c-parent", Parent);
customElements.define("c-grand-parent", GrandParent);
customElements.define("c-great-grand-parent", GreatGrandParent);
type ParentElements = GreatGrandParent | GrandParent | Parent;
const child = new Child();
let currentParent: ParentElements | null = child.parent;
while (currentParent) {
console.log(currentParent); // should be GreatGrandParent | GrandParent | Parent
currentParent = currentParent.parent;
}π Actual behavior
The curerntParent's type is not referred correctly. It became Parent | GrandParent.
π Expected behavior
The currentParent's type should be Parent | GrandParent | GreatGrandParent.
Additional information about the issue
No response
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
BugA bug in TypeScriptA bug in TypeScriptDomain: check: Control FlowThe issue relates to control flow analysisThe issue relates to control flow analysisHelp WantedYou can do thisYou can do this
Activity
Andarist commentedon Aug 22, 2024
With removed redundant pieces (TS playground):
jcalz commentedon Aug 22, 2024
Isn't this just because
GreatGrandParentis a subtype ofChild,Parent, andGrandparentso it can get reduced out of unions with them? If you add more structure toParentandGrandparentthen the problem goes away, right?Andarist commentedon Aug 23, 2024
Same thing happens even if we make all of those classes nominal: TS playground
Andarist commentedon Aug 23, 2024
currentParentinwhile (currentParent), this has two antecedentslet currentParentdeclaration. Since the declared type is a union the assigned value is used as the initial type. Thanks to this antecedent TS getsParent | nullas the potential typegetInitialOrAssignedType(that is meant to be passed togetAssignmentReducedType) herecurrentParent(since it needs to read its.parent) might be and that depends on the same flow loop that we are already in the middle of processing (the compiler starts to compute it at step 1).currentParentisParent | nullwe proceed to read.parentfrom it. This results inGranParent | nullcurrentParentParent | GrandParent | null. Later on, it passes that tocheckTruthinessExpression(called bycheckWhileStatement) so thenullgets discarded.This is 100% a bug but one that I don't know how to fix. I hope the above will help somebody to do it π
Andarist commentedon Aug 26, 2024
Ryan Cavanaugh (@RyanCavanaugh) shouldn't this really be classified as a bug if the CFA incorrectly drops one of the possible types? It's not that this isn't precise enough - it's plain incorrect. Take a look at this extension of the problem:
milkcask commentedon Oct 5, 2024
Last known good: 4.3.0-dev.20210319
Doesn't work since: 4.3.0-dev.20210322
Related #43183
milkcask commentedon Oct 7, 2024
Some progress note.
I can confirm the change in #43183 was the root cause.
checkIdentifieris called withCheckMode.TypeOnlyto get the type of second antecedentConsider:
Call stack:
I wonder where should we apply the fix. And what might be the edge cases.
nmain commentedon Dec 2, 2024
The behavior before #43183 isn't significantly better, as it still fails if you add one more layer:
Playground
milkcask commentedon Feb 20, 2025
It seems we can't bring back the behaviour before #43183. I'm thinking we might need to keep track of what the blocks/flows do with CheckMode. (Or perhaps a new flag?) Also, nmain's observation is interesting as
let p: All | nullis explicitlyAll | null, but the checker somehow determines it's better to infer it.milkcask commentedon Sep 16, 2026
Confirmed it still reproduces on 7.1.0-dev (as of today, d2b20b35), as currentParent narrows to Parent | GrandParent inside the loop. Behaviour is unchanged from the 5.x checker, so it survived the port.
Iterate loop junctions until the narrowed type stops growing
Iterate loop junctions until the narrowed type stops growing
Iterate loop junctions until the narrowed type stops growing
Iterate loop junctions until the narrowed type stops growing
milkcask commentedon Sep 16, 2026
Per the discussion above, I settled on a new flag on the flow loop stack.
In the old js checker, it is one more parallel array next to
flowLoopNodes,flowLoopKeysandflowLoopTypes:and in Go, a field on the stack entry:
So the
reenteredflag records the re-entry. It is set only when the checker computes the flow type of a variable while that variable's own assignment is being evaluated, as incurrentParent = currentParent.parent.The antecedent pass is fundamentally one step of an approximation toward the correct type for this case.
So the fix wraps the antecedent pass in an outer loop: push the loop label flow node (FlowFlags.LoopLabel) onto the flow loop stack, run the pass, and if the reentered flag was set and the resulting union grew, run the pass again, as a fixed-point iteration, until nothing is added. At that point the approximation is exact.
Iterate the antecedent pass so self-assignment in a loop narrows the β¦
Iterate the antecedent pass so self-assignment in a loop narrows the β¦
Iterate antecedent pass so self-assignment in a loop narrows to full β¦
Iterate antecedent pass so self-assignment in a loop narrows to full β¦
Iterate antecedent pass so self-assignment in a loop correctly narrows
Iterate antecedent pass so self-assignment in a loop correctly narrows
Iterate antecedent pass so self-assignment in a loop correctly narrows
Iterate antecedent pass so self-assignment in a loop correctly narrows