-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Narrowing inconsistencies #9260
Comments
#8548 works a bit differently from what you assume. It basically says that in either the when true or when false branch of a type guard for some Let me start with (4). Prior to the I think it is fair to assume you should see the same type at (1). The reason you don't is that (for historical reasons) we don't narrow in the when false branch of a Regarding the |
Thanks @ahejlsberg, that helps me understand #8548 better. So it is applying in the above example, just not where or how I expected. I can see the problem #8548 solves by following its link to #8513. However, by applying to the example above, it seems to introduce problems of its own. Here is a simpler example showing what I mean: type Coin = {type: 'heads', h} | {type: 'tails', t};
function f1(c: Coin) {
if (c.type === 'heads') {
return c.h;
}
else /* must be 'tails' */ {
return c.t;
}
c; // ERROR: unreachable code detected
}
function f2(c: Coin) {
if (c.type === 'heads') {
return c.h;
}
else if (c.type === 'tails') {
return c.t;
}
c; // c is 'heads'
} Assuming that the argument With #8548 in play, function f2b(c: Coin) {
if (c.type === 'heads') {
return c.h;
}
else if (c.type === 'tails') {
return c.t;
}
c.h; // No compiler error
c.t; // ERROR: 't' doesn't exist
// --- If we had checked for 'tails' before 'heads' in the if/else above, we'd get the opposite result: ---
c.h; // ERROR: 'h' doesn't exist
c.t; // No compiler error
} These errors are really artifacts of #8548. They arise from a type inference that does not reflect runtime behaviour, and they are sensitive to code order that has no logical relevance. Personally, I'd rather see the 'unreachable code' error in all the examples above. I'd like to think that there is some exception that could be implemented around #8548 so that it still applies in situations like #8513, but no longer produces artifacts like those here and in #9246 and #9254. |
Suggestion: don't apply #8548 when encountering a type guard whose asserted type exactly matches the control flow type at that point in the code. Here is a example demonstrating the type inference behaviour from before #8548 was merged: // compiled with [email protected]
let x: number | string;
x // x is number | string
x = 1234;
x // x is number
if (typeof x === "string") {
return x; // x is nothing <===== #8513 problem
}
x // x is number
if (typeof x === "number") {
return x; // x is number
}
x // x is nothing Here is the behaviour of the same example currently (i.e. with #8548): // compiled with [email protected]
let x: number | string;
x // x is number | string
x = 1234;
x // x is number
if (typeof x === "string") {
return x; // x is string <===== fixed #8513
}
x // x is number
if (typeof x === "number") {
return x; // x is number
}
x // x is string <===== new #9246/#9254/#9260 problem Here is what I think the behaviour would be if #8548 is not applied when a type assertion exactly matches the current control flow type: // Compiled with hypothetical change to #8548 behaviour
let x: number | string;
x // x is number | string
x = 1234;
x // x is number
if (typeof x === "string") {
return x; // x is string <===== fixed #8513 (still)
}
x // x is number
if (typeof x === "number") { // <===== DON'T apply #8548 here
return x; // x is number
}
x // x is never <===== fixed #9246/#9254/#9260 The last example still fixes #8513, but now also produces the 'expected behaviour' for #9246 and #9254 and #9260. |
I see how that might work for this example, but I'm not sure we could formulate a notion of exactly matches for all the different kinds of type guards we support. For example, it's not clear what would exactly match a truthy or falsy check. Also, I'm not really sure it works in all cases. Here's a twist on the motivating example from #8513: let cond: boolean;
function foo() {
let x: string | number = 0;
x; // number
while (cond) {
x; // number, then string | number
x = typeof x === "number" ? "abc" : x.slice();
x; // string
}
} I think you're arguing We could potentially solve #8513 by only applying #8548 when we know we're analyzing a loop body with incomplete data. But that would require more closely tracking the distinction between the phases of loop analysis. And it still wouldn't solve the initial example in #8548, i.e. the cases where you really want the type guard to function as an assertion because control flow analysis isn't aware of an assignment that has occurred in a nested function. |
@ahejlsberg I don't know the details of the compiler's loop analysis. So speaking purely from my meat-powered type analysis of your loop example, it's unclear to me why the above suggestion would infer Here is the same code expanded a little: let cond: boolean;
function foo() {
let x: string | number = 0;
x; // number
while (cond) {
x; // number | string <===== (1)
if (typeof x === 'number') {
x; // number <===== (2)
x = "abc";
x; // string
}
else {
x; // string <===== (3)
x = x.slice();
x; // string
}
x; // string <===== (4)
}
x; // number | string
} In your example at (1) you've commented |
I'm not sure why that would even be necessary, at least initially. If 'exactly matches' meant literally 'the same type', it would fix all cases of the 'type exhaustion' pattern where a series if That would resolve #9246, #9254, and this issue. Anything more sophisticated than that would be a further improvement that could be addressed later. |
Fixed in #10118. |
@ahejlsberg can this please be re-opened? Some of the reported inconsistencies are still there with Remaining issues with the original example:
Remaining issues with the
Another example with unreachable code: In #8652 it says " function nonReturningA() { // inferred return type is string|number. Shouldn't it be never?
let x = Math.random() > 0.5 ? 'foo' : 42; // x is string|number
if (typeof x === 'string') {
throw x; // x is string
x; // ERROR: unreachable code detected
}
else /* x must be a number */ {
throw x; // x is number
x; // ERROR: unreachable code detected
}
return x; // ERROR: unreachable code detected. Doesn't that imply return type is never?
}
function nonReturningB() { // inferred return type is never
let x = Math.random() > 0.5 ? 'foo' : 42;
if (typeof x === 'string') {
throw x; // x is string
x; // ERROR: unreachable code detected
}
else if (typeof x === 'number') {
throw x; // x is number
x; // ERROR: unreachable code detected
}
return x; // x is never, and return type is never, implying this is unreachable
// no error reported here. Why doesn't tsc report this as unreachable code?
}
EDIT: I should add that I really care about getting the 'unreachable code detected' error because it instantly proves that the prior control flow branches are exhaustive. Conversely, 'reachable' code below what should be an exhaustive control flow block prompts me to look for errors in my control flow logic. |
Agreed. If
We only report unreachable code errors based on the structure of the code, i.e. when we are 100% certain the code is unreachable in all circumstances. A |
@ahejlsberg I've submitted issue #10145 for the remaining type guard narrowing issue. For the unreachable code part, I understand the rationale you've just given, the compiler is being conservative with its inferences. But the different compiler behaviour for |
I was about to post about 'never' in switch statements and unreachable code in a new issue and found this. I guess I'll instead take the time to +1 the idea of a strict analysis mode (or perhaps assume-TypeScript-caller mode?). In cases where TypeScript calls TypeScript, it's somewhat frustrating to make concessions for where the type system 'could have been given the wrong information' (see also my issue #10765 in a similar vein, where all code is TS but we rely upon the user providing valid type information). In line with A. Hejlsberg's comments, perhaps we could at least /warn/ here? I'll see if it's possible to add a lint to TSLint, for now. Edit: a related issue is palantir/tslint#661 Original issue: type X = 'a' | 'b';
function sw(x: X): number {
switch (x) {
case 'a':
return 1;
case 'b':
return 2;
default:
return 'string' // Error: Type 'string' is not assignable to type 'number'.
}
} Expected behavior: Error on the line 'default' indicating unreachable code (or better, an over-exhaustive match). Actual behavior: Error on the returned string line. |
TypeScript Version:
nightly (1.9.0-dev.20160619-1.0)
Code
Three functions that narrow three types to see how narrowing behaves.
foo1
andfoo2
are identical apart from which type guards they use.Expected behavior:
never
, like it is at (7)Actual behavior:
See the comments in the code.
I wrote this code trying to understand what was going on in #9246 and #9254, and now I think I'm more confused. Firstly,
foo1
andfoo2
have identical logic, but tsc infers types differently in each. Secondly, I couldn't work out how to invoke the 'type guards as assertions' (#8548) behaviour. I thought it should kick in after (1) and (4), where the types have been exhausted and then another type guard appears. But control flow analysis never infersnever
in either function. We do getnever
inferred at (7), but then #8548 doesn't seem to apply at (8) where I expected it should.The text was updated successfully, but these errors were encountered: