Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Variable modified in loop supposedly "referenced directly or indirectly in its own initializer" #43047

Closed
jtbandes opened this issue Mar 2, 2021 · 14 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@jtbandes
Copy link
Contributor

jtbandes commented Mar 2, 2021

Bug Report

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.

TS playground

  let foo: {value:number} | undefined;

  function bar() {
    foo = {value: 1};

    while (true) {
      const currentFoo = foo;  // error here
      if (currentFoo) {
        const currentValue = currentFoo.value;  // error here
        foo = {value: currentValue+1};
        if (currentValue > 1) break;
      }
    } 
  }

🔎 Search Terms

implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer

Possibly related issues: #16892, #22390, #35546

🕗 Version & Regression Information

  • This is the behavior in every version I tried
@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Mar 9, 2021
@RyanCavanaugh
Copy link
Member

currentValue is referenced in its own initializer during the second iteration of the loop.

@jtbandes
Copy link
Contributor Author

jtbandes commented Mar 9, 2021

It is? How so?

Also note that adding a type annotation to currentValue on line 9 fixes the problem:

TS playground

  let foo: {value:number} | undefined;

  function bar() {
    foo = {value: 1};

    while (true) {
      const currentFoo = foo; // no error
      if (currentFoo) {
        const currentValue: number = currentFoo.value; // type annotation added here -- no error
        foo = {value: currentValue+1};
        if (currentValue > 1) break;
      }
    } 
  }

@RyanCavanaugh
Copy link
Member

It is? How so?

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

@jtbandes
Copy link
Contributor Author

jtbandes commented Mar 9, 2021

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...

@RyanCavanaugh
Copy link
Member

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
Copy link
Contributor Author

jtbandes commented 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
Copy link
Member

"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
Copy link
Contributor Author

jtbandes commented Mar 9, 2021

I guess the best I can hope for then is a clearer error message :)

@garyo
Copy link

garyo commented 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):

declare function foo(x: string | undefined): Promise<string>

async () => {
  let bar: string | undefined = undefined;
  do {
    const baz = await foo(bar); // ERROR HERE
    bar = baz
  } while (bar)
}

In this case, the return type of foo is always Promise<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.

@jtbandes
Copy link
Contributor Author

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...

@bvallee-thefork
Copy link

Facing the same issue.

Same as with #43708, it works when removing the await and Promise<>.

The temporary workaround we're using: explicitly casting the "circular" value.

Example (with the indirectly in its own initializer error):

declare function myQuery(input: { lastId: number | undefined }): Promise<{ entities: number[] }>;

async function myFunc(): Promise<void> {
  let lastId: number | undefined = undefined;

  while (true) {
    const { entities } = await myQuery({
        lastId,
    });

    lastId = entities[entities.length - 1];
  } 
}

Workaround (no error):

declare function myQuery(input: { lastId: number | undefined }): Promise<{ entities: number[] }>;

async function myFunc(): Promise<void> {
  let lastId: number | undefined = undefined;

  while (true) {
    const { entities } = await myQuery({
        lastId,
    });

    lastId = entities[entities.length - 1] as number | undefined;
  } 
}

@echentw
Copy link

echentw commented Oct 4, 2022

Not sure if this is helpful, but here's a minimal repro (without async-await syntax)

playground link

let foo: string | null = null;
while (true) {
    const currentFoo = foo; // error: 'currentFoo' implicitly has type 'any' because ...
    foo = currentFoo;
}

@user72356
Copy link

Here is another very simple repro:

function test()
{
    let test1: number | undefined = 42;

    if (test1 === undefined)
    {
        return;
    }

    for (let i = 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)
        const test2 = test1 + 1; 

        // If I remove this line everything is fine
        test1 = test2;
    }
}

@RyanCavanaugh
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

6 participants