Typescript: Strict null checks and flow control of returned functions

Created on 20 Jun 2016  路  7Comments  路  Source: microsoft/TypeScript

TypeScript Version:

Version 1.9.0-dev.20160619-1.0

Code

function doThing (x?: { prop: boolean }) {
  if (x == null) {
    return function () {
      throw new TypeError('Not correctly set up')
    }
  }

  return function () {
    return x.prop
  }
}
{
  "compilerOptions": {
    "strictNullChecks": true
  }
}

Expected behavior: Successful compilation.

Actual behavior:

index.ts(9,12): error TS2532: Object is possibly 'undefined'.

I honestly wasn't sure what this exact feature would be called to find a duplicate, and I know I've logged a similar issues issues in the past. Let me know if a duplicate does exist. In any case, I would expect flow control to have understood the undefined case was already handled. I suppose this could be related to the fact the arguments are mutable? Is it possible to use flow control analysis to understand nothing re-assigns the value (in fact, nothing else even executes) after that return?

Fixed Suggestion

Most helpful comment

A proposed fix is to allow a const modifier on function parameters to avoid having to do that.

:+1: to this, there are many places in our code that would benefit from this. It would satisfy tsc's control flow analysis and we could get rid of a bunch of dummy local vars that are there to help tsc but are making the code less readable.

All 7 comments

Flow control is not preserved across function boundaries, because TypeScript cannot be sure when the function is going to be invoked. It is discussed here.

@kitsonk Thanks, I have read that issue before. However, I disagree with the generality. I believe there are multiple cases that can be handled here. For instance, const x is handled across function boundaries because it can not be re-assigned.

Function Expression (Handled Today):

const y: { prop: string } | undefined = { prop: 'tets' }

if (y) {
  let test = () => {
    return y.prop
  }
}

Conditional (Handled Today):

let y: string | number = 'test'

if (typeof y === 'string') {
  y.charAt(0)
  // Using `y = 10` within this block would cause `y.charAt` to become an error.
}

y = 10

So, given these are handled correctly today, what is the difference between this and other variables that are already block scoped and never modified? For instance:

function test (x: string | number) {
  if (typeof x === 'string') {
    setTimeout(function () {
      console.log(x.length)
    })
    // Note the **return** here, just as in the original example, which means nothing else in this block can modify `x`.
    return
  }
}

However, even given the above fact, if return were not called but x never modified within the block of the function (which is what x is scoped to) you can also infer x never changes.

The problem is that we don't realize that there are no assignments to x in the body, so this code is effectively indistinguishable from the unsafe version:

function doThing (x?: { prop: boolean }) {
  if (x == null) {
    return function () {
      throw new TypeError('Not correctly set up')
    }
  }
  let danger = function () {
    return x.prop;
  }
  x = null;
  return danger;
}

One workaround is to write const y = x at the top of your function. A proposed fix is to allow a const modifier on function parameters to avoid having to do that.

@RyanCavanaugh Thanks. Is it possible to include it as part of the flow control logic that currently exists? If it can error when y is re-assigned (becoming number | string) within an if block (earlier comment), I imagine it makes sense for this behaviour to be expanded for functions within the same block.

Edit: Sorry, to clarify, this was definitely intended as a feature request and not a bug report - I just filled out what it asked of me in the OP.

A proposed fix is to allow a const modifier on function parameters to avoid having to do that.

:+1: to this, there are many places in our code that would benefit from this. It would satisfy tsc's control flow analysis and we could get rid of a bunch of dummy local vars that are there to help tsc but are making the code less readable.

a workaround: explcitly cast to a non-undefined union type, as per:

function doThing (_x?: { prop: boolean }) {
  let x = _x as { prop: boolean };
  if (x == null) {
    return function () {
      throw new TypeError('Not correctly set up')
    }
  }

  return function () {
    return x.prop
  }
}

This should be fixed by https://github.com/Microsoft/TypeScript/pull/10357. If a function parameter is never assigned to, it is treated as a const. so the sample in the OP compiles fine under TS 2.0.2 or later.

Was this page helpful?
0 / 5 - 0 ratings