typescripttypecheckingnarrowing

Type narrowing & never functions


When using type narrowing with TypeScript, never functions do not react the same way every time.

const getValue = () => {
  const value = process.env.VARIABLE
  if (!value) process.exit(1)
  return value
}

In the example below, getValue inferred type is () => string, because value is typed string | undefined, and TypeScript is able to identify that the remaining code after process.exit is unreachable, and the return value is always a string.

const exit = () => {
  return process.exit(1)
}

const getValue = () => {
  const value = process.env.VARIABLE
  if (!value) exit()
  return value
}

In this second example, exit is typed () => never, and getValue inferred type is () => string | undefined, because TypeScript is unable to detect that the rest of the function is unreachable if value does not exists. However, if I add a return before the exit call (so return exit()), it works as desired.

Why does it works like this? Is there a way to make it work in the second case?


Solution

  • TypeScript's support for functions that return never, as implemented in microsoft/TypeScript#32695, only works if the function type is explicitly annotated to return never. It would be nice if the type checker could just infer that, but allowing that would make control flow analysis much harder to implement so that it performs well. As it says in the above-liked PR, "this particular rule exists so that control flow analysis of potential assertion calls doesn't circularly trigger further analysis".

    So, to get the behavior you're looking for, you should annotate exit as being of type () => never, like this:

    const exit: () => never = () => {
      return process.exit(1)
    }
    

    and then it just works:

    const getValue = () => {
      const value = process.env.VARIABLE
      if (!value) exit()
      return value
    }
    // const getValue: () => string
    

    Playground link to code