Closed EugeneZ closed 5 months ago
As a workaround I usually write a disjoint union
type Dog = Animal & {
tag: 'Dog',
breed: string
};
type Cat = Animal & {
tag: 'Cat',
food: string
};
function readAnimals(animal: Cat | Dog) {
if (animal.tag === 'Dog') {
animal.breed // ok
}
}
@gcanti Thanks. Yes, this workaround fixes the problem for me, and its what I have resorted to, but I am forced to mutate data that I'd rather not mutate. I shouldn't have to do it to get it to typecheck correctly.
Encountered this problem too today. This really needs to get fixed. Is a blocker for us expanding our Flow coverage.
My team would appreciate a fix for this as well.
This behaviour is unsound either way, so I recommend just to use any
Our use case is similar to the animal example posted above, except we have around ~10 common properties I don't want to duplicate across the eight types that compose our disjoint union. This was an effort on my part to keep the code DRY. I consider it a reasonable use case for intersections. Is there a more preferred way to keep types DRY?
It's not specific to intersections, it actually shouldn't work even without them
The issue I am reporting is unique to intersections, I believe.
I think you might be confusing it for another issue (the one you linked on StackOverflow). That or I am the one that is confused.
For clarification, here are some more examples. They're similar to the one above, but now I'm using typeof
to be extra specific. This changes the behavior of the bug, but it still disappears if you remove the intersection. Now, instead of displaying an error during the test itself, the test does not error (good) but when I attempt to access a property of one of the members of the intersection, I don't get a Flow error, even though I should!
Broken, even though I'm being very specific with my type tests
What I mean is that the bug is not that it doesn't work with intersection, it's that it works without them.
@vkurchatkin What do you mean? Why shouldn't you be able to access a property of an object that has a type composed through an intersection?
@c10b10 you shouldn't be able to access a property on union type, if it's not present on all bracnhes
It's not that we/I want to access a property that's on one branch (that's obviously unsound), it's that I want to be able to do a type refinement via a test on that property.
That's basically as unsound. You can't my assumptions about a property if it's not a part of type. So, checking it is not enough for refinement.
I'm not making any assumptions about a property in that example, so I'm afraid I don't follow... Could you provide an example of the way in which this inference is unsafe?
Sorry for bumping up an old thread, but it looks like we still observe the same behavior as of today:
@EugeneZ's example (slightly modified to explicitly check for undefined
) is still raising an error https://flow.org/try/#0C4TwDgpgBAggdgSwLYEMA2UC8UDeAoKKBAEwC4o4BXJAIwgCc8BfAbjz1EigBEB7AcyyxEqDADJcBKDXoQIZKAGdg9BHH7M2HcNADCKYEPjJ0UCfkIAzXrwXLV6ze0uU4AY2AJecKLJTFjUUUAChQRdHJ9QwAfHgEASklCBEsoYM4IXktQ8LQAOhk5YkTMUqgAcldiCEs1eXLEiygmZiA
If I'm following your thoughts correctly @vkurchatkin, it's not @EugeneZ's example failing that is a bug, but rather the second example not failing that is in itself a bug.
However I too have trouble understanding why this is unsound. Since by definition we are checking whether this property is defined first, I would assume it's not the same as accessing it.
Assuming the following code:
function handleCatOrDog(catOrDog) {
if (typeof(catOrDog.breed) !== 'undefined') {
handleDog(catOrDog);
} else {
handleCat(catOrDog);
}
}
How would someone type it without modifying catOrDog
's code to include a type
discriminator? If it came from an external API for example?
Apologies for not having re-read https://flow.org/en/docs/types/unions/#toc-disjoint-unions-with-exact-types before, that explains why using non-exact types will not allow for refinements.
Along with https://github.com/facebook/flow/issues/2626#issuecomment-267449133, I actually came up with something close to what I was searching for.
Leaving it here in case it's useful to other people stumbling upon this thread: https://flow.org/try/#0PTAEAEDMBsHsHcBQAXAngBwKagIIDsBLAWwENpQBeUAbwB9FRQCATALlDwFciAjTAJ0S0AvgG5EKDNgAisAOaUa9Rj36ZMbUAGdk-AnjkAaBqAB05-MTJCxEtFlABhEskV0TkWLE069B44zmppak0DbiiJCceADGyASweKBqJMzOyAAUMS7s6QCUNCYxiVqw0JimcHIZAOQAFpjQcKDZyDV54sISUbHxicmYqbLVzPLswwXURSVlFVW1dQSgo3Ltnd3RcQlJKcwhZFoZJIShuS6gtKAThYwEkKBHJ2Smnt6TJoy76Y9W0HkmwlAjS02DuD2Ov1MqnUzEmoBAoAAKostKAAAZ3NHaOqwTjQZigdD8WA8Eg8aCoDiwVx8DgwjQAfg+AyG8h+oX+jC6wiAA
Workaround with spreads
type Animal = {|
id: number
|};
type Dog = {|
...Animal,
breed: string
|};
type Cat = {|
...Animal,
food: string
|};
function readAnimals(animal: Cat | Dog) {
if (animal && animal.breed) {
}
}
This is intended for inexact objects, because otherwise you might access some property that Flow doesn't know its existence, but incorrectly get void instead
Flow REPL example
There are so many union/intersection issues on GitHub, but none of them looked like this one except a closed issue #1759, figured that might be outdated by now.
Basically this code fails:
But this works:
The only difference is that the
Animal
type has been copied-and-pasted into theDog
andCat
types. This makes it harder to write DRY types.For reference, the error in the first example: