Skip to content

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

Closed as not planned

Description

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

Activity

  1. RyanCavanaugh commented on Mar 9, 2021

    @RyanCavanaugh
    Member

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

  2. jtbandes commented on Mar 9, 2021

    @jtbandes
    ContributorAuthor

    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;
          }
        } 
      }
  3. RyanCavanaugh commented on Mar 9, 2021

    @RyanCavanaugh
    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

  4. jtbandes commented on Mar 9, 2021

    @jtbandes
    ContributorAuthor

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

  5. RyanCavanaugh commented on Mar 9, 2021

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

  6. jtbandes commented on Mar 9, 2021

    @jtbandes
    ContributorAuthor

    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?

  7. RyanCavanaugh commented on Mar 9, 2021

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

  8. jtbandes commented on Mar 9, 2021

    @jtbandes
    ContributorAuthor

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

  9. garyo commented on Apr 16, 2021

    @garyo

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

  10. jtbandes commented on Apr 18, 2021

    @jtbandes
    ContributorAuthor

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

  11. bvallee-thefork commented on Jul 5, 2021

    @bvallee-thefork

    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;
      } 
    }
  12. echentw commented on Oct 4, 2022

    @echentw

    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;
    }
    
  13. user72356 commented on Nov 14, 2022

    @user72356

    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;
        }
    }
  14. RyanCavanaugh commented on Nov 19, 2022

    @RyanCavanaugh
    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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design LimitationConstraints of the existing architecture prevent this from being fixed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions