typescript

Why does a closure un-narrow a narrowed const type in TypeScript?


I wrote a function that returns a function that solves world hunger, but only if anyone is still hungry, otherwise null.

If and only if anyone is still hungry, the inner function is created, and encloses the const variable about whether anyone is still hungry.

During the execution of this function, we know logically that the variable has been narrowed to either true or "fileNotFound.

And issue #56908 by @ahejlsberg admits it should work:

We currently preserve type refinements in closures for const variables

But TypeScript complains.

The playground shows the types:

type IsStillHungry = true | false | 'fileNotFound' | undefined

function solveWorldHungerStrategy(opts: { isAnyoneStillHungry: IsStillHungry }) {

  const condition = opts.isAnyoneStillHungry

  if (condition) {
    
    condition
    // ^? const condition: true | "fileNotFound"

    function pretendToSolveWorldHunger() {

      condition
      // ^? const condition: boolean | "fileNotFound" | undefined

    }

    return pretendToSolveWorldHunger

  }

  return null

}

Solution

  • TypeScript never maintains narrowing of closed-over variables inside the body of function declarations (statements), which are hoisted to the top of the enclosing function or global scope. In general that means the function can be called before the narrowing takes place. Indeed, inside the same pull request you quoted, microsoft/TypeScript#56908, it says:

    Type refinements are not preserved in inner function and class declarations (due to hoisting).

    If you change the declaration pretendToSolveWorldHunger from a statement to an expression, then the narrowing is preserved:

    function solveWorldHungerStrategy(
      opts: { isAnyoneStillHungry: boolean | 'fileNotFound' | undefined }
    ) {
      const condition = opts.isAnyoneStillHungry
      if (condition) {
        condition
        // ^? const condition: true | "fileNotFound"
        const pretendToSolveWorldHunger = function () {
          condition
          // ^? const condition: true | "fileNotFound"
        }
        return pretendToSolveWorldHunger
      }
      return null
    }
    

    Of course a const that holds a primitive value cannot be reassigned, so in theory TypeScript could decide to preserve any narrowings of such const variables, regardless of hoisting. But this is a missing feature of TypeScript, as described in the open feature request microsoft/TypeScript#36913.

    Playground link to code