TypeScript Version: 2.3 (Playground)
Code
Using strictNullChecks.
interface Foo {
optional?: number;
}
interface Bar {
foo?: Foo;
}
function test(bar: Bar) {
if (bar.foo!.optional) {
let num: number = bar.foo.optional;
}
if (bar.foo && bar.foo.optional) {
let num: number = bar.foo.optional;
}
}
Expected behavior:
No errors or warnings.
Actual behavior:
num in first if block is number | undefinedbar.foo in first if block can be undefined
this is behaving as intended. the ! operator is not a narrowing operator. it just muffles the error. so every time you use the variable you need to reapply !. if you want to narrow, use one of the narrowing patterns, like bar.foo && bar.foo.optional is suggested above.
if block if either foo or optional is undefined.bar.foo!.optional inside the block can be undefined, even though I just checked that it's not. The very fact that I used ! on foo breaks the type guard on optional. See the second if block below.
The ! operator does not change the type.. it just tells the compiler, i know it is null | undefined, but i want to access it anyways.. it is similar to your regular type assertion, think of:
var x: number | string;
if (typeof (x as number).toFixed === "function") {
(x as number).toFixed(); // need to cast again
var y: number = x; // error, x is number | string
}
@mhegazy what's our excuse for not unwrapping this, though?
If we retrieved a property of an identifier, there's no reason to not consider that a truthiness guard of the same identifier. It's clearly not null / undefined
The assertion has no scope, it can appear in the middle of an expression, or in the middle of an if block, it is not clear what that means to the type for the rest of the block.. other narrowing constructs do have clear scoping semantics /control flow implications.
We did discuss this before, but i can not find the issue at the moment.
This isn't about applying the ! operator to the rest of the block. It's about this:
function test(bar: Bar) {
if (bar.foo!.optional) {
let num: number = bar.foo.optional; // Should not error!
}
The dotted property access on bar.foo should act as an equivalent type guard to if (bar.foo) {, because the fact we got inside the if block without crashing means it's not null/undefined
Well, it's actually about this:
if (bar.foo!.optional) {
let num: number = bar.foo!.optional; // "Type 'number | undefined' is not assignable to type 'number'"
}
although it was also surprising that I have to use foo!. twice, even inside the if block.
If the ! operator is used before a . or (), it could be considered a narrowing operator.
For example, this:
function test (a: { b?: { c: string } }) {
console.log(a.b!.c)
console.log(a.b.c)
}
could be considered as being more-or-less equivalent to:
function test (a: { b?: { c: string } }) {
if (a.b == null) throw new TypeError()
console.log(a.b.c)
console.log(a.b.c)
}
as the second a.b.c is not reachable if a.b!.c's erasure of null|undefined does not happen to be proved true at runtime.
But the throw itself is done by the engine..... and is an expression throw...
Has this ever been resolved?
Most helpful comment
If the
!operator is used before a.or(), it could be considered a narrowing operator.For example, this:
could be considered as being more-or-less equivalent to:
as the second
a.b.cis not reachable ifa.b!.c's erasure ofnull|undefineddoes not happen to be proved true at runtime.But the throw itself is done by the engine..... and is an expression throw...