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

implicitly 'any' type error despite no circularity in assignment and being assigned a template literal #54790

Closed
vassudanagunta opened this issue Jun 26, 2023 · 6 comments Β· Fixed by #54795
Labels
Bug A bug in TypeScript Help Wanted You can do this
Milestone

Comments

@vassudanagunta
Copy link

Bug Report

πŸ”Ž Search Terms

  • 7022
  • directly or indirectly in its own initializer

πŸ•— Version & Regression Information

This changed between versions 4.3.5 and 4.4.2

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

function uniqueID (id: string | undefined, seenIDs: { [key: string]: string }): string {
    if (id === undefined) {
        id = '1'
    }
    if (!(id in seenIDs)) {
        return id
    }
    for (let i = 1; i < Number.MAX_VALUE; i++) {
        const newID = `${id}-${i}`      // ERROR 7022, despite no apparent circularity, despite template literal
        if (!(newID in seenIDs)) {
            return newID
        }
    }
    throw Error('heat death of the universe')
}

πŸ™ Actual behavior

const newID = `${id}-${i}` yields error: 'newID' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.(7022)

πŸ™‚ Expected behavior

const newID = `${id}-${i}` has no error and newID has inferred type string:

  • there is no circular reference that i can fathom
  • even if there was, the template literal it is assigned has type string. See anyToString proof in playground link and code below.

πŸ€” Some Analysis

Alternate versions to help isolate the bug

// removed possible undefined from `id` type
function uniqueID_sans_undefined (id: string, seenIDs: { [key: string]: string }): string {
    if (!(id in seenIDs)) {
        return id
    }
    for (let i = 1; i < Number.MAX_VALUE; i++) {
        const newID = `${id}-${i}`      // inferred as string as expected
        if (!(newID in seenIDs)) {
            return newID
        }
    }
    throw Error('heat death of the universe')
}

// newID initialized outside of the loop
function uniqueID_init_outside_loop (id: string | undefined, seenIDs: { [key: string]: string }): string {
    if (id === undefined) {
        id = '1'
    }
    if (!(id in seenIDs)) {
        return id
    }
    let newID                           // any
    for (let i = 1; i < Number.MAX_VALUE; i++) {
        newID = `${id}-${i}`            // narrowed to string as expected
        if (!(newID in seenIDs)) {
            return newID
        }
    }
    throw Error('heat death of the universe')
 }

// remove use of template literal to show possibility of two disinct bugs
function uniqueID_init_sans_template (id: string | undefined, seenIDs: { [key: string]: string }): string {
    if (id === undefined) {
        id = '1'
    }
    if (!(id in seenIDs)) {
        return id
    }
    for (let i = 1; i < Number.MAX_VALUE; i++) {
        const newID = id                // ERROR 7022, despite no apparent circularity
        if (!(newID in seenIDs)) {
            return 'not a unique id just demonstrating type inferrence bug'
        }
    }
    throw Error('heat death of the universe')
}

// prove template literal has type string 
// even if embedded expression has type any
function anyToString (x: any) {  // inferred return type string
    const y = `${x}`             // inferred type string
    return y
}

🧬 Possibly Related Bugs / Duplicates

Three other open issues i found have similar behavior:

  • error 7022
  • no error if undefined or null removed from type
  • no error if initialization does not happen in a loop

Two of them have different "blame" changes. The third may have been introduced by the same change, but it makes some addition claims, and does not show the issue with template literals this issue does.

issue when introduced
#33191 before 3.3.3333 (oldest version in Playground)
#45638 between 4.3.5 and 4.4.2
this issue between 4.3.5 and 4.4.2
#48708 after 4.6.4
@RyanCavanaugh
Copy link
Member

Possible red herrings removed:

function uniqueID (id: string | number, seenIDs: object) {
    id = 'a';
    for (let i = 1; i < 3; i++) {
        const newID = `${id}`;
        if (newID in seenIDs) { }
    }
}

@Andarist
Copy link
Contributor

Can be simplified even further:

function uniqueID (id: string | number, seenIDs: object) {
    id = 'a';
    for (let i = 1; i < 3; i++) {
        const newID = id;
        if (newID in seenIDs) { }
    }
}

@typescript-bot
Copy link
Collaborator

The change between origin/release-4.2 and origin/release-4.5 occurred at e064817.

@RyanCavanaugh RyanCavanaugh added Bug A bug in TypeScript Help Wanted You can do this labels Jun 26, 2023
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Jun 26, 2023
@RyanCavanaugh
Copy link
Member

I'm not sure how easy this is going to be to fix. It looks like the circularity is as follows, using @Andarist 's example:

  • What is the type of newID ?
  • What is the type of id ?
  • It's either 'a', or possibly the result of an assignment which might occur later in the for loop, if the loop-back edge of the loop is reachable. Is it reachable?
  • Depends on whether there are any unconditionally-reached exits of the loop. Is the if statement considered unconditional?
  • It depends on whether or not newID has a value we think is always in seenIDs. What's the type of newID ?
  • Circularity reached, πŸ’₯

@Andarist
Copy link
Contributor

This is the same issue as the mentioned #48708 . This manifestation here is somewhat easy to get fixed (see my PR here) but that other one (and likely many others like it) are not as simple. My fix is somewhat targeted and a more general fix could just fix all issues at once but I don't know how to prepare a more general fix and the proposed one still feels like an improvement from the usability point of view πŸ˜‰

@vassudanagunta
Copy link
Author

@Andarist good to see the PR that fixes this. I have a question from curiosity, a desire to understand. Answer only if it's easy and you can spare a minute.

Since the loop has no other assignment to newID other than a value of type string, why does reachability of the "loop-back edge" even matter? Why would the in condition even matter. All the in condition could possibly do is narrow the type to something even narrower than string, right?

Likewise I don't understand why declaring newId with type any outside the loop somehow fixes it (See uniqueID_sans_undefined under Analysis). That make no sense! Or why removing undefined from its starting type in uniqueID_sans_undefined's function signature matters, when on the first line of the function it is narrowed to string. Confused, but that could be my aging brain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Help Wanted You can do this
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants