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 modified in loop supposedly "referenced directly or indirectly in its own initializer" #43047
The following code produces this error on two lines:
'currentFoo' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.(7022)
This is a reduced repro case from a real code snippet. It seems like maybe the error is arising when the compiler tries to refine the types of these variables by looking at assignment expressions. But the result is unintuitive and unhelpful.
Expected behavior:
At worst, the compiler should just give currentFoo the same type as the declaration of foo, and this would be better than the current behavior.
letfoo: {value:number}|undefined;functionbar(){foo={value: 1};while(true){constcurrentFoo=foo;// no errorif(currentFoo){constcurrentValue: number=currentFoo.value;// type annotation added here -- no errorfoo={value: currentValue+1};if(currentValue>1)break;}}}
Because the kinds of values that can inhabit foo are determined what can possibly be in currentValue, which in turn picks up a value from currentFoo at the top of the loop, which in turn depends on what foo might be
Would it be reasonable to automatically fall back to the type that foo had before it acquired the dependence on currentValue?
Even sticking to the very first type foo is given ({value:number} | undefined) would be enough to make the definition of currentFoo typecheck without causing problems downstream. You would also have to apply the same logic to currentValue, inferring its type without any refinement of currentFoo beyond the first time it's used.
I know my terminology above isn't very precise, but my mental type-checking thought process goes:
foo is a {value:number}|undefined
currentFoo is initially {value:number}|undefined, and inside if(currentFoo) it's known to be {value:number}
therefore currentFoo.value is a number
therefore currentValue is a number ✅
Basically, I think that this case can be solved by applying less advanced type inference than what the compiler is currently trying to do...
Silently falling back to the declared type is going to just make things more confusing in the cases where a circularity exists, because then it will often look like narrowing is broken in much more subtle ways.
"Do this thing if it happens to make the program pass, otherwise do this other thing" logic is O(2^n) on the nesting level of your code, which isn't conducive to good compile times.
#43708 is considered a dup of this bug, and I don't want to beat a dead horse (I do have a workaround) but in principle it seems like ts ought to be able to figure that case out (this is the minimal repro from that bug report):
In this case, the return type of foo is alwaysPromise<string> so in the const baz line, the type of baz is definitely known to be string, right? (Independent of the current type of bar, which does switch from undefined to string.) So it seems to me that there should be no recursion there.
Reacted by Garnet, Jacob Bandes-Storch, Benoit Vallée, whzx5byb, Konrad, Guillaume Simard, Ian Clarkson and 𝒪𝑜𝓂
Your example seems a little different, because it involves async/await: if I change Promise<string> to string and remove the await, it works. I agree it feels like the same kind of issue, but my original example doesn't involve async. I wonder how the async ends up triggering the problem...
functiontest(){lettest1: number|undefined=42;if(test1===undefined){return;}for(leti=0;i<10;i++){// test1: Object is possibly 'undefined'. ts(2532)// 'test2' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.ts(7022)consttest2=test1+1;// If I remove this line everything is finetest1=test2;}}
To recap: TypeScript can avoid erroring in some (but not all) cases of circularities. This does mean that it's possible to write two programs which are slightly different, one of which will have a circularity error, one of which will not. The cases that we can't avoid issuing a circularity error on are, generally speaking, impossible to fix without rearchitecting the entire type checker. The existence of cases where we successfully avoid issuing a circularity error in the presence of a possible circularity doesn't change any of this.
In general we won't make things broadly worse for the sake of consistency so suggestions to "error always" will not be accepted.
Activity
RyanCavanaugh commentedon Mar 9, 2021
currentValueis referenced in its own initializer during the second iteration of the loop.jtbandes commentedon Mar 9, 2021
It is? How so?
Also note that adding a type annotation to currentValue on line 9 fixes the problem:
TS playground
RyanCavanaugh commentedon Mar 9, 2021
Because the kinds of values that can inhabit
fooare determined what can possibly be incurrentValue, which in turn picks up a value fromcurrentFooat the top of the loop, which in turn depends on whatfoomight bejtbandes commentedon Mar 9, 2021
Would it be reasonable to automatically fall back to the type that
foohad before it acquired the dependence oncurrentValue?Even sticking to the very first type
foois given ({value:number} | undefined) would be enough to make the definition ofcurrentFootypecheck without causing problems downstream. You would also have to apply the same logic to currentValue, inferring its type without any refinement ofcurrentFoobeyond the first time it's used.I know my terminology above isn't very precise, but my mental type-checking thought process goes:
foois a{value:number}|undefinedcurrentFoois initially{value:number}|undefined, and insideif(currentFoo)it's known to be{value:number}currentFoo.valueis a numbercurrentValueis a number ✅Basically, I think that this case can be solved by applying less advanced type inference than what the compiler is currently trying to do...
RyanCavanaugh commentedon Mar 9, 2021
Silently falling back to the declared type is going to just make things more confusing in the cases where a circularity exists, because then it will often look like narrowing is broken in much more subtle ways.
jtbandes commentedon Mar 9, 2021
I can see that being confusing, but only in the case that the result has a type error. Is it possible to try the fallback and only use it if it works?
RyanCavanaugh commentedon Mar 9, 2021
"Do this thing if it happens to make the program pass, otherwise do this other thing" logic is
O(2^n)on the nesting level of your code, which isn't conducive to good compile times.jtbandes commentedon Mar 9, 2021
I guess the best I can hope for then is a clearer error message :)
garyo commentedon Apr 16, 2021
#43708 is considered a dup of this bug, and I don't want to beat a dead horse (I do have a workaround) but in principle it seems like ts ought to be able to figure that case out (this is the minimal repro from that bug report):
In this case, the return type of foo is always
Promise<string>so in theconst bazline, the type ofbazis definitely known to bestring, right? (Independent of the current type ofbar, which does switch fromundefinedtostring.) So it seems to me that there should be no recursion there.jtbandes commentedon Apr 18, 2021
Your example seems a little different, because it involves async/await: if I change
Promise<string>tostringand remove theawait, it works. I agree it feels like the same kind of issue, but my original example doesn't involve async. I wonder how the async ends up triggering the problem...bvallee-thefork commentedon Jul 5, 2021
Facing the same issue.
Same as with #43708, it works when removing the
awaitandPromise<>.The temporary workaround we're using: explicitly casting the "circular" value.
Example (with the
indirectly in its own initializererror):Workaround (no error):
echentw commentedon Oct 4, 2022
Not sure if this is helpful, but here's a minimal repro (without async-await syntax)
playground link
user72356 commentedon Nov 14, 2022
Here is another very simple repro:
RyanCavanaugh commentedon Nov 19, 2022
All of these examples are circularities 🙃
To recap: TypeScript can avoid erroring in some (but not all) cases of circularities. This does mean that it's possible to write two programs which are slightly different, one of which will have a circularity error, one of which will not. The cases that we can't avoid issuing a circularity error on are, generally speaking, impossible to fix without rearchitecting the entire type checker. The existence of cases where we successfully avoid issuing a circularity error in the presence of a possible circularity doesn't change any of this.
In general we won't make things broadly worse for the sake of consistency so suggestions to "error always" will not be accepted.