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?
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.
Most helpful comment
:+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.