First, my code is below:
// @flow
type RawPos = {|lng: number, lat: number|}
type MapPos = {|getLng: function, getLat: function|}
type Pos = RawPos | MapPos;
function getPosition(value: Pos) {
if (typeof value.lng === 'number') {
const lat: number = value.lat;
} else {
const getLng: function = value.getLng;
}
}
I assume this would pass the flow check, but flow told me NOPE. Check the screenshot below.

Is there a better way to handle this situation?
Also, I tried to use in operator to refine, use $Exact to define the exact object type, but still not work.
// @flow
type RawPos = {|lng: number, lat: number|};
type MapPos = {|getLng: function, getLat: function|};
type Pos = RawPos | MapPos;
function getPosition(value: Pos) {
if (value.lng) { // obviously this will fail when lng === 0
const lat: number = value.lat;
} else if (value.getLng) {
const getLng: function = value.getLng;
}
}
i tried if (value.lng != undefined) but that's not good either, i guess type refinements work for only very specific tests, like if (something) to make T from ?T, and if (something == 'foo') to make 'foo' from string.
So I guess I have to refine the both types by myself. The official examples here did refine union types by a specific value(true) like you said.
Thank you for your advise. @madbence
Why not use disjoint unions?
/* @flow */
type RawPos = {| kind: 'raw', lng: number, lat: number|}
type MapPos = {| kind: 'map', getLng: function, getLat: function|}
type Pos = RawPos | MapPos;
function getPosition(value: Pos) {
if (value.kind === 'raw') {
const lat: number = value.lat;
} else {
const getLng: function = value.getLng;
}
}
The MapPos typed value is imported from external module, there are no kind property to identify them.
At the moment there are two ways to refine unions of object types. If you can use disjoint unions (where each object has a sentinel property with a singleton type), that's definitely the cleanest way.
Property existence checks also work, but currently we only detect the form if (o.p) { ... }. The original question has if (typeof o.p === "number") { ... } which could work, but isn't implemented today.
One issue with the o.p condition is that for for numbers and strings the inverse case isn't a negative condition. That is, when value.pos is true, we're definitely have a RawPos, but when value.pos is false, it might be 0 (false-y) so it can still be a RawPos or a MapPos.
Supporting more patterns for refinement of unions-of-object-types would be a very welcome addition, but not a current focus of the core team at the current moment, unfortunately.
@samwgoldman Thank you for your answer, that's very helpful. I have changed my code to the following and that work well.
// @flow
type RawPos = {|lng: number, lat: number|};
type MapPos = {|getLng: function, getLat: function|};
type Pos = RawPos | MapPos;
function getPosition(value: Pos) {
if (value.getLng) { // if I put value.lng first, might failed on zero, not good enough
const getLng: function = value.getLng;
const getLat: function = value.getLat;
} else {
const lat: number = value.lat;
const lng: number = value.lng;
}
}
Quick heads-up. You're using function with a small f. This is probably a mistake, and you should be using Function instead.
@nmn Ah, Thank you for correction.
just because I stumbled over it, @ioslh's code does also require the exact types! (without them the refinement doesn't work either)
Based on @samwgoldman's comment, it sounds like what I've been trying to do is impossible:
// @flow
type Rect = {|
x: number,
y: number,
width: number,
height: number,
|};
type Line = {|
x1: number,
y1: number,
x2: number,
y2: number,
|};
const lineOverlapsRect = (line: Line, rect: Rect): boolean => true;
const rectOverlapsRect = (rectA: Rect, rectB: Rect): boolean => true;
export const shapeOverlapsRect = (shape: Rect | Line, rect: Rect): boolean => {
if (typeof shape.width === 'number') {
return rectOverlapsRect((shape: Rect), rect);
} else {
return lineOverlapsRect((shape: Line), rect);
}
};
Maybe I just need to approach this problem from a different angle, but I have a hard time seeing how this isn't a major stumbling block. My experience with flow so far suggests that my idea of just applying it on top of my extant codebase was pretty naive.
Related to this, but I think probably undeserving of its own issue:
Disjoint unions which share the same field names but with different types don't refine as you might expect.
type Data =
| { type: 'a', data: {| value: string |} }
| { type: 'b', data: {| something: string |} }
| { type: 'c', data: {| value: number |} }
;
// const data: Data = { type: 'a', data: { value: 'blue' } };
const data: Data = { type: 'c', data: { value: 5 } };
if (data.value) {
// I would expect data.value is `string | number`, but is actually just `mixed`
(data.value: mixed);
(data.value: string|number); // Error
if (typeof data.value === 'string') {
(data.value: string);
} else {
(data.value: mixed);
(data.value: number); // Error, flow still thinks data.value is mixed
}
}
And on a similar note, you might expect that inside of the else{} data.value is mixed - string, but I suppose flow doesn't track mixed that way
Most helpful comment
At the moment there are two ways to refine unions of object types. If you can use disjoint unions (where each object has a sentinel property with a singleton type), that's definitely the cleanest way.
Property existence checks also work, but currently we only detect the form
if (o.p) { ... }. The original question hasif (typeof o.p === "number") { ... }which could work, but isn't implemented today.One issue with the
o.pcondition is that for for numbers and strings the inverse case isn't a negative condition. That is, whenvalue.posis true, we're definitely have aRawPos, but whenvalue.posis false, it might be 0 (false-y) so it can still be aRawPosor aMapPos.Supporting more patterns for refinement of unions-of-object-types would be a very welcome addition, but not a current focus of the core team at the current moment, unfortunately.