You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Variable isn't narrowed within a capturing closure #35124
The more I see this issue come up, the more I wonder if it’s possible to do better. The analyzer can’t look into function calls, but would it be possible to notice that the variable is not accessed after the closure is created, so the last narrowing can be maintained? Seems like it should be easy enough to determine that syntactically, since the function is already inline.
If the function modifies the variable itself, that’s trickier, but that problem seems similar to loop analysis on the surface.
Jordi Oliveras Rovira (@j-oliveras) which section of the linked issue are you referring to? I'm okay with closing this as a dupe if it's covered by the other issue, but I couldn't find my case on first glance.
Anyway, I understand your point. I was hoping the compiler would realize that i doesn't get modified after the assignment of 0 as Bruce Pascoe (@fatcerberus) suggests. But if that's not in the cards, I can easily deal with it.
This is kind of the inverse of #9998, but not a direct dupe I don’t think. #9998 is about what happens with local narrowings when you call a function (answer: nothing, the compiler is optimistic and assumes no side effects); the issue here is about what happens to narrowings inside a function that closes over the narrowed variable (answer: narrowings are discarded at function boundaries, with the exception of IIFEs).
This particular issue has come up many times in the past though.
I don't think there's anything better than the current behavior in this case.
I assume the suggestion isn't: "uncallable functions should have no type checking inside their implementation" (the arrow function in the example code is uncallable because it's neither immediately invoked nor assigned to anything)
So, assuming the function is meant to be callable, what better could be done here? i isn't const, so as pointed out by Jordi Oliveras Rovira (@j-oliveras), one could do i = undefined and call the function for a nice runtime error. Is the suggestion: "we should suppress a warning if the situation warned about doesn't happen to occur in the actual code"? Like this:
functionfoo(i: number|undefined){i+1;// no error! foo() is only called safely}foo(1);foo(2);// foo(undefined); // commented out
and then if you uncomment foo(undefined), an error then appears inside the foo() implementation? That's some spooky action at a distance. You'd only ever get warnings if you wrote tests I guess.
Since that doesn't make much sense, I'm guessing there's some other suggestion being made here, but I'm not seeing it. IIFEs are already inlined because it's one of the few places we can say that the unsafe situation is known to be impossible and not just fortuitously avoided. What better behavior are we hoping for here?
So, assuming the function is meant to be callable, what better could be done here? i isn't const, so as pointed out by Jordi Oliveras Rovira (@j-oliveras), one could do i = undefined and call the function for a nice runtime error.
If i is a local variable and the outer function returns before i is reassigned (which normal CFA can already tell us), and it's not modified inside the closure, then we already know, a priori--just by looking at the AST--that the variable can't possibly be modified further. If it's modified inside the closure, then the same challenges apply as for loop analysis I would think.
functionfoo(i: number|undefined){i+1;// no error! foo() is only called safely}foo(1);foo(2);// foo(undefined); // commented out
I would expect this to error regardless of my proposed behavior, because undefined is potentially being added to a number. Also, there's no closure involved in that example.
If Bruce Pascoe (@fatcerberus) is accurately describing the problem, can we change the example code to something that demonstrates the relevant issue? Like, maybe this:
functionfoo(){leti: number|undefined;i=0;return(k: number)=>k===i+1;// error that i is possibly undefined }console.log(foo()(1));// trueconsole.log(foo()(0));// false
It seems like the rule is "If a closure is lexically after all assignments to a bare identifier, then the last narrowing can apply".
You can break this, though:
functionadderWrapper(n?: number){functionsetValue(v?: number){n=v;}n=n??0;return{
setValue,add: (m: number)=>m+n}}constk=adderWrapper(0);k.setValue(undefined);k.add(3);// NaN
So the rule would need to be amended to "All assignments to a bare identifier occur lexically and CFA-before the closure expression", though possibly that could be broken too.
Reacted by Bruce Pascoe, Felix Geisendörfer and Steven
Ryan Cavanaugh (@RyanCavanaugh) nailed what I was shooting for. From my outsider perspective, it definitely seems like this could be enabled using a combination of existing lexical and CFA data, though it would be interesting to explore the edge cases, if any indeed exist.
Ryan Cavanaugh (@RyanCavanaugh) the makeAdder example is exactly the situation in which I ran into this problem. I also had a function with an optional argument that was given a default value in the function body.
I ended up working around the issue by simply assigning the default value in the function declaration:
I'm guessing this behavior is because named functions are "lifted" (or whatever it is called) and can in theory be called prior to their definition. It feels like this could be detected, but maybe that is a lot of complexity for something that can be worked earound with lambdas?
Is this the same issue? Or should I create a separate?
functionfooOk(){letr="b";()=>r;}functionfooError(){letr// Variable 'r' implicitly has type 'any' in some locations where its type cannot be determined.(7034)r="b";()=>r;// Variable 'r' implicitly has an 'any' type.(7005)}
Activity
j-oliveras commentedon Nov 15, 2019
Duplicate of #9998.
Typescript warns you that the
ican be undefined when the function is called:Playground.
fatcerberus commentedon Nov 15, 2019
The more I see this issue come up, the more I wonder if it’s possible to do better. The analyzer can’t look into function calls, but would it be possible to notice that the variable is not accessed after the closure is created, so the last narrowing can be maintained? Seems like it should be easy enough to determine that syntactically, since the function is already inline.
If the function modifies the variable itself, that’s trickier, but that problem seems similar to loop analysis on the surface.
felixge commentedon Nov 15, 2019
Jordi Oliveras Rovira (@j-oliveras) which section of the linked issue are you referring to? I'm okay with closing this as a dupe if it's covered by the other issue, but I couldn't find my case on first glance.
Anyway, I understand your point. I was hoping the compiler would realize that
idoesn't get modified after the assignment of0as Bruce Pascoe (@fatcerberus) suggests. But if that's not in the cards, I can easily deal with it.fatcerberus commentedon Nov 15, 2019
This is kind of the inverse of #9998, but not a direct dupe I don’t think. #9998 is about what happens with local narrowings when you call a function (answer: nothing, the compiler is optimistic and assumes no side effects); the issue here is about what happens to narrowings inside a function that closes over the narrowed variable (answer: narrowings are discarded at function boundaries, with the exception of IIFEs).
This particular issue has come up many times in the past though.
jcalz commentedon Nov 15, 2019
I don't think there's anything better than the current behavior in this case.
I assume the suggestion isn't: "uncallable functions should have no type checking inside their implementation" (the arrow function in the example code is uncallable because it's neither immediately invoked nor assigned to anything)
So, assuming the function is meant to be callable, what better could be done here?
iisn'tconst, so as pointed out by Jordi Oliveras Rovira (@j-oliveras), one could doi = undefinedand call the function for a nice runtime error. Is the suggestion: "we should suppress a warning if the situation warned about doesn't happen to occur in the actual code"? Like this:and then if you uncomment
foo(undefined), an error then appears inside thefoo()implementation? That's some spooky action at a distance. You'd only ever get warnings if you wrote tests I guess.Since that doesn't make much sense, I'm guessing there's some other suggestion being made here, but I'm not seeing it. IIFEs are already inlined because it's one of the few places we can say that the unsafe situation is known to be impossible and not just fortuitously avoided. What better behavior are we hoping for here?
fatcerberus commentedon Nov 15, 2019
If
iis a local variable and the outer function returns beforeiis reassigned (which normal CFA can already tell us), and it's not modified inside the closure, then we already know, a priori--just by looking at the AST--that the variable can't possibly be modified further. If it's modified inside the closure, then the same challenges apply as for loop analysis I would think.felixge commentedon Nov 15, 2019
Yes, it's meant to be callable. I just omitted that from my example for the sake of brevity.
fatcerberus commentedon Nov 15, 2019
I would expect this to error regardless of my proposed behavior, because
undefinedis potentially being added to a number. Also, there's no closure involved in that example.jcalz commentedon Nov 15, 2019
If Bruce Pascoe (@fatcerberus) is accurately describing the problem, can we change the example code to something that demonstrates the relevant issue? Like, maybe this:
RyanCavanaugh commentedon Nov 15, 2019
If I had to draw the most realistic picture of a warning on a closed-over variable that really shouldn't exist, it'd look like this:
It seems like the rule is "If a closure is lexically after all assignments to a bare identifier, then the last narrowing can apply".
You can break this, though:
So the rule would need to be amended to "All assignments to a bare identifier occur lexically and CFA-before the closure expression", though possibly that could be broken too.
fatcerberus commentedon Nov 15, 2019
Ryan Cavanaugh (@RyanCavanaugh) nailed what I was shooting for. From my outsider perspective, it definitely seems like this could be enabled using a combination of existing lexical and CFA data, though it would be interesting to explore the edge cases, if any indeed exist.
[-]Closure assumes wrong type[/-][+]Retain certain narrowings of closed-over identifiers[/+][-]Retain certain narrowings of closed-over identifiers[/-][+]Variable isn't narrowed within a capturing closure[/+]felixge commentedon Nov 18, 2019
Ryan Cavanaugh (@RyanCavanaugh) the
makeAdderexample is exactly the situation in which I ran into this problem. I also had a function with an optional argument that was given a default value in the function body.I ended up working around the issue by simply assigning the default value in the function declaration:
MicahZoltu commentedon May 20, 2023
Perhaps this should be a separate issue, but is there any way to make this do the right thing with
constvariables and named functions?I'm guessing this behavior is because named functions are "lifted" (or whatever it is called) and can in theory be called prior to their definition. It feels like this could be detected, but maybe that is a lot of complexity for something that can be worked earound with lambdas?
n9 commentedon May 25, 2023
Is this the same issue? Or should I create a separate?
Playground