Skip to content

Type exhaustiveness not working with in operator guard #48256

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

Closed
lorenzodallavecchia opened this issue Mar 14, 2022 · 5 comments
Closed

Type exhaustiveness not working with in operator guard #48256

lorenzodallavecchia opened this issue Mar 14, 2022 · 5 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@lorenzodallavecchia
Copy link

Bug Report

πŸ”Ž Search Terms

in narrow narrowing guard

πŸ•— Version & Regression Information

  • This changed starting from version 4.3.2

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

let obj!: { foo: string };
if ("foo" in obj) {
  obj.foo;
} else {
  assertNever(obj); 
}
function assertNever(n: never) {}

πŸ™ Actual behavior

The call to assertNever is marked error because obj is still typed { foo: string }.

Interestingly it does work if the type of obj has more than one branch. The problem seems present only when there is a single branch in the union type.

Also, it does work when using a type guard function.

πŸ™‚ Expected behavior

obj should have type never in the else branch, as the presence of foo property has been ruled out by the guard.

@fatcerberus
Copy link

fatcerberus commented Mar 14, 2022

in only narrows unions, a single type is not considered a union:

let obj!: { foo: string } | { foo: number };
if ("foo" in obj) {
    obj.foo;
} else {
    assertNever(obj);  // now it works
}

@lorenzodallavecchia
Copy link
Author

@fatcerberus I see.
In this particular case I am guarding a non-union to protect the branching code against future additions to the type. Today it only has one branch, but I may extend it in the future and would like assertNever to warn me.

However, this is a regression and may indicate that something is broken more deeply in TypeScript.
After all, any type can be seen as the union of itself with never.

The fact that it works with two branches that are both matched by the guard re-inforces the idea that this is some kind of "edge condition" bug.

@fatcerberus
Copy link

fatcerberus commented Mar 14, 2022

It's not a regression since this is how in-based narrowing has always behaved.
I was wrong about the above - see Ryan's comment below.

After all, any type can be seen as the union of itself with never.

In principle this is true, however in practice treating all types as unions now would be a massive breaking change, since the distinction matters in other places too (distributive behavior of conditional types, e.g.). It is not a goal of TypeScript's design to be mathematically sound.

The fact that it works with two branches that are both matched by the guard re-inforces the idea that this is some kind of "edge condition" bug.

As mentioned, the distinction that matters here is whether the type being guarded is a union type or not, and is by design. See #38963.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Mar 14, 2022
@RyanCavanaugh
Copy link
Member

This was an intentional change; see #38608 (comment)

@lorenzodallavecchia
Copy link
Author

Thanks @RyanCavanaugh.
I have read the references issues and I understand why the behavior was changed.
I will work around the issue for this simple case by just using a custom guard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

3 participants