TypeScript Version: 3.4.1
Search Terms:
ternary statement vs if statement
ternary statement type widening
Code
function main(): number {
const x: any = {}
return x.y ? x.y : undefined // no error
}
function main2(): number {
const x: any = {}
if (x.y) {
return x.y
} else {
return undefined // error
}
}
Expected behavior:
It appears that returning a value from a ternary statement has a different behavior than and if-statement. The ternary statement type is widened and resolved before returning. This causes some unsafe type behavior. I would expect the two programs above to work the same and throw and error because the type definition of the function returns a number and yet we are definitely returning undefined.
Actual behavior:
The entire ternary statement returns as an "any" type.
Playground Link: playground link
Related Issues:
I’m thinking the inconsistency here is that, in the first case, the entire ternary expression is inferred as any | undefined which collapses to just any; the compiler therefore sees a single return any and all is right with the world (even though it’s not). In the second case there are two return sites: one is return any which again is fine, and one is return undefined which is not.
It's not even about return; the same problem occurs here:
let n: number;
const x: any = {}
n = x.y ? x.y : undefined // no error
const x: any = {}
if (x.y) {
n = x.y
} else {
n = undefined // error
}
The consistent proposal would be that when a source expression of an assignment (or equivalent) is a ternary operator, we should check that both of its true/false operands are assignable to the target. But this just opens up more "consistency" problems:
// m: any
let m = x.y ? x.y : undefined;
n = m; // This is still OK, good grief!
There's not any principled place along the rabbit hole to stop going down further, plus "fixing" this would a substantial breaking change
Yeah, I knew the return wasn’t the source of the issue; it was more a way to highlight that the compiler sees the ternary as a single expression of type TrueCase | FalseCase rather than separate expressions as with the if-else.
This union behavior is generally sound; it just so happens that unioning any with any other type loses all information about the other type. any is kind of a “viral” type in that regard.
This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.
@RyanCavanaugh, the any type is always going to create unsafe types but we can be pragmatic here. The ternary statement is functionally equivalent to the if-else statement with a hoisted variable. Why should one be more type-safe than the other? What if all ternary statements were treated by the typescript compiler the same as an if-else statement? We'd at least catch some obvious errors...
I think the challenge is that a ternary isn't really a statement but an expression. An expression must have a single type; in this case that type happens to be any | undefined which is unsound simply by the nature of any. Changing this, I suspect, would be very difficult: what happens when you have a ternary in the middle of a statement, e.g. as an argument to a function? What happens when there are several ternaries in the same statement, as I've been known to stack them to heavens:
In order to type check the above the way you suggest, the compiler would basically have to duplicate the statement and individually type check it for every combination of true/false for every single ternary in the statement. You'd have a combinatorial explosion.
The current behavior is, I suspect, the best we're going to get.
I see. That is a bummer, but thanks for looking into it!
Most helpful comment
It's not even about
return; the same problem occurs here:The consistent proposal would be that when a source expression of an assignment (or equivalent) is a ternary operator, we should check that both of its true/false operands are assignable to the target. But this just opens up more "consistency" problems:
There's not any principled place along the rabbit hole to stop going down further, plus "fixing" this would a substantial breaking change