Skip to content
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

Noncrashing property access through non-null assertion operator should narrow that property to NonNullable #15655

Open
LeviticusMB opened this issue May 8, 2017 · 11 comments
Labels
Bug A bug in TypeScript Help Wanted You can do this
Milestone

Comments

@LeviticusMB
Copy link

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

@RyanCavanaugh RyanCavanaugh added the Bug A bug in TypeScript label May 8, 2017
@mhegazy
Copy link
Contributor

mhegazy commented May 8, 2017

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.

@mhegazy mhegazy added Working as Intended The behavior described is the intended behavior; this is not a bug and removed Bug A bug in TypeScript labels May 8, 2017
@LeviticusMB
Copy link
Author

LeviticusMB commented May 9, 2017

  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

@mhegazy
Copy link
Contributor

mhegazy commented May 9, 2017

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
}

@RyanCavanaugh
Copy link
Member

@mhegazy what's our excuse for not unwrapping this, though?

@RyanCavanaugh RyanCavanaugh removed the Working as Intended The behavior described is the intended behavior; this is not a bug label May 9, 2017
@RyanCavanaugh
Copy link
Member

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

@mhegazy
Copy link
Contributor

mhegazy commented May 9, 2017

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.

@RyanCavanaugh
Copy link
Member

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

@mhegazy mhegazy added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels May 11, 2017
@mhegazy
Copy link
Contributor

mhegazy commented May 11, 2017

related #9640

@LeviticusMB
Copy link
Author

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.

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature and removed In Discussion Not yet reached consensus labels Oct 10, 2017
@Jessidhia
Copy link

Jessidhia commented Oct 11, 2017

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

@ackvf
Copy link

ackvf commented Jun 28, 2018

Has this ever been resolved?

@RyanCavanaugh RyanCavanaugh changed the title Type guards do not work when using the non-null assertion operator Noncrashing property access through non-null assertion operator should narrow that property to NonNullable Jun 25, 2021
@RyanCavanaugh RyanCavanaugh added Bug A bug in TypeScript and removed Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Jun 25, 2021
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Jun 25, 2021
@RyanCavanaugh RyanCavanaugh added the Help Wanted You can do this label Jun 25, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Help Wanted You can do this
Projects
None yet
Development

No branches or pull requests

5 participants