Typescript: Type guards do not work when using the non-null assertion operator

Created on 8 May 2017  路  11Comments  路  Source: microsoft/TypeScript

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 | undefined
  • bar.foo in first if block can be undefined

skarmavbild 2017-05-08 kl 12 58 06

Awaiting More Feedback Suggestion

Most helpful comment

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...

All 11 comments

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.

  1. Why? There is no way I can enter the first if block if either foo or optional is undefined.
  2. And even so, TypeScript still thinks 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.

skarmavbild 2017-05-09 kl 09 33 48

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?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dlaberge picture dlaberge  路  3Comments

bgrieder picture bgrieder  路  3Comments

Roam-Cooper picture Roam-Cooper  路  3Comments

seanzer picture seanzer  路  3Comments

weswigham picture weswigham  路  3Comments