Skip to content

Narrowed const variables are widened in block-scoped closures #61158

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

Open
kirkwaiblinger opened this issue Feb 10, 2025 · 2 comments
Open

Narrowed const variables are widened in block-scoped closures #61158

kirkwaiblinger opened this issue Feb 10, 2025 · 2 comments
Labels
Help Wanted You can do this Possible Improvement The current behavior isn't wrong, but it's possible to see that it might be better in some cases
Milestone

Comments

@kirkwaiblinger
Copy link

kirkwaiblinger commented Feb 10, 2025

πŸ”Ž Search Terms

narrowing, cfa, closure,

πŸ•— Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about n/a

⏯ Playground Link

https://www.typescriptlang.org/play/?noUncheckedIndexedAccess=true&target=99&ts=5.8.0-dev.20250209#code/CYUwxgNghgTiAEYD2A7AzgF3mgXNjMAligOYDcAUBYQGbwAUa8AvK-AOQ1JLsCU8AbwrwRiVJmwBBCIShNm2SqPg0ArijAZCqeACMowevyHLlydFi5I8nbuxaLhogL5VlajVp0luh409MxCxVuGyt7BTRpWTQlFwpXChAADwAHJBgsAWcgA

πŸ’» Code

declare const s: string;

if (s === 'foo') {
    function bad() {
        const foo: 'foo' = s;
    }
}

// in non-strict code bad() might be accessible outside of the narrowed branch,
// so make sure we're strict
export {}

πŸ™ Actual behavior

error trying to use the variable as its narrowed value.

πŸ™‚ Expected behavior

No error. bad() is a local function only reachable within the scope in which s has been definitely narrowed to 'foo'.

Additional Information

Found organically in typescript-eslint/typescript-eslint#10182 (comment) (with a narrowed member access corollary)

@RyanCavanaugh RyanCavanaugh added Help Wanted You can do this Possible Improvement The current behavior isn't wrong, but it's possible to see that it might be better in some cases labels Feb 13, 2025
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Feb 13, 2025
@felipe-gustavo
Copy link

felipe-gustavo commented Feb 18, 2025

Same for "strictNullChecks":

declare const foo: {
  bar?: symbol[]
}

function main() {
  if (!foo.bar) {
    throw new Error()
  }

  const fooBar = () => {
    foo.bar
    //  ^? (property) bar?: symbol[] | undefined

    foo.bar.push(Symbol())
    // ~~~~ 'foo.bar' is possibly 'undefined'. (18048)
  }
}

TS Playground


Workaround

Redefine the variable in the narrowed scope

declare const foo: {
  bar?: symbol[]
}

function main() {
  if (!foo.bar) {
    throw new Error()
  }

  const { bar } = foo

  const fooBar = () => {
    bar
    //  ^? const bar: symbol[]

    bar.push(Symbol())
  }
}

In @kirkwaiblinger's case:

declare const s: string;

if (s === 'foo') {
    const aliasForS = s
    function bad() {
        const foo: 'foo' = aliasForS;
    }
}

@milkcask
Copy link

Same for

(() => {
    if (s !== 'foo') return;
    const sAlias = s;
    function bad() {
        const foo: 'foo' = s;
    }

    function good() {
        const foo: 'foo' = sAlias;
    }
})()

TS Playground

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Help Wanted You can do this Possible Improvement The current behavior isn't wrong, but it's possible to see that it might be better in some cases
Projects
None yet
Development

No branches or pull requests

4 participants