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

Newly introduced type narrowing via discriminators on .filter does not work in this case #59654

Closed
ffMathy opened this issue Aug 16, 2024 · 9 comments
Labels
Duplicate An existing issue was already created

Comments

@ffMathy
Copy link

ffMathy commented Aug 16, 2024

πŸ”Ž Search Terms

"type narrowing", "discriminator"

πŸ•— Version & Regression Information

  • This never changed. It was introduced as part of TypeScript 5.5, which introduced type narrowing on .filter operations.

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/C4TwDgpgBAglC8UDeAoKUAmBLAzgYwCcsBbLAOwENgB7AgLigCIZGUBfFFUSKAIQWRpMuQiXJVaDRr1Ycu4aAGEBqdNnxFSlGgQD8UxbM7doAERVD1orRL1TTRznmpkcwKBQIEKIAWQgA7rBePgA8cAA+fAB8ABQAlADcKM6u7gBmWAA2wBAEEBgwIb6Int4gAHSZOXmxAB4I0VB1FVaa4joI8IjMjPEoAPQDABZ5EAA0UNW5+YXFULhQzl4QeMBZvtTpUCZMMADaALqsKS5uUNTAowRF5X6BweWhylGmcUmnaVPZMwUA8lc8rcfAJLtdgZVprUGvAmi02mJtLQuj1DB8htcJt8arMAeD5otyMt8msNhdtrtGLFFBFTPEjowoAAjACu7hww2oLKyGGZ0EYigZgwGWHcLjJwwoYEgrimyOoYGAWBcFCywg0iNsUDABAVeSVEBwQA

πŸ’» Code

type A = {
  discriminator: "A"
}

type B = {
  discriminator: "B"
}

type C = {
  discriminator?: "C"
}

type D = {
  discriminator?: "D"
}


const array = new Array<A | B>();
const filteredArray = array.filter(x => x.discriminator === "A")
//here, filteredArray is correctly of type "A[]"

const otherArray = new Array<C | D>();
const filteredOtherArray = otherArray.filter(x => x.discriminator === "C");
//here, filteredOtherArray is incorrectly of type "(C|D)[]" but should be "C[]"
//it only happens for optional discriminator properties

πŸ™ Actual behavior

In the example, filteredOtherArray is incorrectly of type "(C|D)[]"

πŸ™‚ Expected behavior

In the example, filteredOtherArray should be of type C[].

Additional information about the issue

No response

@gismya
Copy link

gismya commented Aug 16, 2024

And interestingly enough, faking a filter with flatMap() does work.

const flatMappedOtherArray = otherArray.flatMap(x => x.discriminator === "C" ? x : []); // Type is C[]

@Andarist
Copy link
Contributor

To infer a type predicate TypeScript has to prove both that the narrowed-down type is C and that a type C can't end up in the false branch. In other words, TS has to prove this:

declare const x: C | D;

if (x.discriminator === "C") {
  x; // C
} else {
  x; // D (aka C is 100% eliminated from reaching this branch)
}

In this situation, this isn't something that can be proved. If you inspect the code above u might see that x in the else branch is still C | D. { discriminator: undefined } is a valid C so by checking .discriminator === "C" the compiler can only narrow it down to C in the if branch but it can't rule out that C can't reach the else branch.

TLDR: type predicates have to be exhaustive, this type predicate candidate isn't.

@ffMathy
Copy link
Author

ffMathy commented Aug 16, 2024

Thanks a lot @Andarist for trying to explain this to me. But I still don't get it actually. Can you explain it like I'm 5, or simplify the explanation even more?

What I don't understand is this: If I directly check on discriminator being "C" in a filter, why would that inference be any different than if I just did it in an if-statement?

In other words, if I check that the discriminator is "C", wouldn't that be known for sure, like in your example? And then, how does doing that in a .filter make it different?

@Andarist
Copy link
Contributor

Take a look at this TS playground. The else branch still contains C there.

And then, how does doing that in a .filter make it different?

You might find this comment helpful. Type predicates must prove the function can't return false when called with your C.

@ffMathy
Copy link
Author

ffMathy commented Aug 16, 2024

Aha, I think I understand now.

So it's because they both have "undefined" in common, and therefore none of them can be ruled out?

@ffMathy
Copy link
Author

ffMathy commented Aug 16, 2024

But this should be fixable, right? Because in the filter function, I am not using the "else" case at all. I'm filtering only on the exact matches.

@Andarist
Copy link
Contributor

So it's because they both have "undefined" in common, and therefore none of them can be ruled out?

Not quite. It doesn't matter that D also has an optional property. Take a look at this playground.

It happens solely because it's possible to get false back from this call:

const predicateCandidate = (x: C | D) => x.discriminator === "C"
const c: C = { discriminator: undefined }
predicateCandidate(c) // false

But this should be fixable, right? Because in the filter function, I am not using the "else" case at all. I'm filtering only on the exact matches.

This would be a feature request to add something new so that filter's callback could infer from those new kinds of loose type predicates. Today filter can only infer from type predicates, type predicates have specific requirements and TS can only infer type predicates when those are met. The inference here is decouple from filter. You should think about what happens here in steps. The first step is to determine what's the type of the callback and only then filter infers based on what was resolved.

@nmain
Copy link

nmain commented Aug 16, 2024

Also see #15048 for discussion of the typeguards separately from the filter issue.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Aug 16, 2024
@typescript-bot
Copy link
Collaborator

This issue has been marked as "Duplicate" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Aug 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

6 participants