flow returns wrong type - flowtype

I have this piece of code:
/* #flow */
import { List } from 'immutable';
type NotMapped = {|
first: Array<number>,
second: number,
|};
type Mapped = {|
first: List<number>,
second: number,
|};
const notMapped: NotMapped = {
first: [1, 2, 3],
second: 10,
};
const map = () => {
const first = List(notMapped.first);
return { ...notMapped, first };
}
const result: Mapped = map();
I want result to beMapped type but I get:
{|
first: List<number> | Array<number>,
second: number
|}
How come that flow thinks it might be Array<number> when I explicitly set first as List(notMapped.first)? It only works if I set return { second: notMapped.second, first };, but this isn't a solution because I have large amount of data and I cannot set every item.
I have checked and this is well-known issue, but when spreading types, not object with assigned type. Is there any solution for this?

Related

Flow type a generic function that groups any type that has at least an ID

I'm trying to type with Flow a function that maps As to Bs, where the only restrictions are:
B contains an A
A has at least an id property which is a string
Apart from that, A can be any object, and, in this situation, B is well known.
I want to type the function with a generic/polymorphic type that so the type checker knows that you will get an array of objects containing the A and B that matches.
My attempt below does not give me any type error, but I don't think it is correct either.
Will love to understand how to properly type this so you can get the most guarantees.
type B = {A: {id: string}}
const BContainsA = (id: string) => (b: B) =>
b.A.id === id
type MapResult<T> = {
AsWithBs: Array<{ A: T, B: B }>,
AsWithoutBs: string[],
}
const mapAsToBs = <T>(
As: { ...T, id: string }[],
Bs: B[]
): MapResult<T> => {
return As.reduce(
(result, a) => {
const b = Bs.find(BContainsA(a.id))
if (!b) {
result.AsWithoutBs.push(a.id)
return result
}
result.AsWithBs.push({ A: a, B: b })
return result
},
{ AsWithBs: [], AsWithoutBs: [] }
)
}
mapAsToBs([{pos:2,id: '1'},{pos:1,id: '11'}],[{A:{id: '1'}}])
Seems that all I had to do was to add a constraint to the generic type like this:
const mapAsToBs = <T:{id: string}>(
As: T[],
Bs: B[]
): MapResult<T> => {
}
It is indeed documented, but the syntax is so unintuitive and the explanation is so short, that I would have never guessed it just by reading it.
You can check how it works as expected here

Function with union argument indirectly causes inexplicable errors on union members

In the following block of code, flow errors occur on the OuterY and OuterZ type definitions only when the getInnerValues function is present.
The errors complain that "Y" and "Z" are incompatible with "X". For example: "string literal Y is incompatible with string literal X.".
/* #flow */
type Inner<T> = { value: T };
type OuterX = { inner: Array<Inner<"X">> };
type OuterY = { inner: Array<Inner<"Y">> };
type OuterZ = { inner: Array<Inner<"Z">> };
type Outer = OuterX | OuterY | OuterZ;
// If the next line is present, errors occur on
// lines 6 and 7 complaining that "Y" and "Z" are
// incompatible with "X". When the next line is
// commented out, the errors go away. Why??
const getInnerValues = (outer: Outer) => outer.inner.map(inner => inner.value);
Why is this happening?
Click here to see the issue on flow.org/try
Click here to see the same issue with stricter typing on flow.org/try
Flow doesn't realize that there exists an inner property of type {value: string} for all the possible cases of Outer. One way to fix this is to type the function to accept an object with the expected type:
(Try)
/* #flow */
type Inner<T> = { value: T };
type OuterX = { inner: Array<Inner<"X">> };
type OuterY = { inner: Array<Inner<"Y">> };
type OuterZ = { inner: Array<Inner<"Z">> };
type Outer = OuterX | OuterY | OuterZ;
// no errors
const getInnerValues = (outer: {inner: Array<{value: string}>}) =>
outer.inner.map(inner => inner.value);
Another way to do this (probably the better way) is to redefine Outer as type which accepts a type parameter. Then you can generically type your getInnerValues function to accept generic Outer instances:
(Try)
/* #flow */
type Inner<T> = { value: T };
type OuterX = { inner: Array<Inner<"X">> };
type OuterY = { inner: Array<Inner<"Y">> };
type OuterZ = { inner: Array<Inner<"Z">> };
type Outer<T> = {
inner: Array<Inner<T>>
}
// no errors
const getInnerValues = <T>(outer: Outer<T>) => outer.inner.map(inner => inner.value);

Higher-order function returning generic function

So in this basic example (tryflow):
// basic identity function example with generic type
type Foo = { prop: number};
type Bar = { prop: string };
const foo: Foo = { prop: 1 };
const bar: Bar = { prop: 'a' };
function identity<T>(same: T): T {
return same;
}
// here identity acts as (Foo) => Foo
const foo2: Foo = identity(foo);
// and here it's (Bar) => Bar
const bar2: Bar = identity(bar);
My identity function, using generics, takes whatever type is given to it. As arguments are bound to it, T becomes first Foo and then Bar.
What I want is a higher-order function which returns a generic function. I can write a higher-order function which uses generics (tryflow):
type IdentityFunction = <T>(self: T) => T;
// error here
const baseId: IdentityFunction = (same) => same;
// ^ Cannot assign function to
// `baseId` because `T` [1] is
// incompatible with `T` [2] in
// the return value.
type Foo = { prop: number};
type Bar = { prop: string };
const foo: Foo = { prop: 1 };
const bar: Bar = { prop: 'a' };
function makeIdentity(func: IdentityFunction): IdentityFunction {
return func;
}
const identity: IdentityFunction = makeIdentity(baseId);
const foo2: Foo = identity(foo);
const bar2: Bar = identity(bar);
For me, this approach makes the most sense. I'm honestly not sure why I get this error. How can T be incompatible with itself? Is it because a type is never explicitly applied to T? It's somehow indeterminate so it just can't be used for anything? But then, isn't that the whole point of generics? Anyway, I'm sure I'm just missing some subtle point of the type system, or maybe I'm going about this the wrong way. Any help appreciated.
You need to generically type your baseID function so Flow knows what you expect as argument and return type. It seems like Flow doesn't use the type of IndentityFunction when trying to figure out what the baseId function is really doing.
(Try)
type IdentityFunction = <T>(self: T) => T;
// no more error
const baseId: IdentityFunction = <S>(same: S): S => same;
type Foo = { prop: number};
type Bar = { prop: string };
const foo: Foo = { prop: 1 };
const bar: Bar = { prop: 'a' };
function makeIdentity(func: IdentityFunction): IdentityFunction {
return func;
}
const identity: IdentityFunction = makeIdentity(baseId);
const foo2: Foo = identity(foo);
const bar2: Bar = identity(bar);
You can simplify the instantiation of baseId to:
const baseId = <S>(same: S): S => same;
And flow still understands what's going on here.
This behavior is a little confusing and I wonder if there is a good reason for it. You would think that it could take what's on the lefthand side and apply it to the function on the right (especially in simple cases like this one). Maybe it has to do with how flow sees the righthand expression? If anyone else has an idea, I'd love to hear it.
Either way, I tend to avoid declaring the type of functions on the lefthand side of declarations. Not as a rule, I just rarely want to declare the type of a function somewhere besides the function itself.

Optional group of keys

I have to make two keys optional index and remove in an object. However if one is provided the other must be there. So its like this:
type Props = {
isSettings: boolean,
} | {
index: number,
remove: $PropertyType<FieldProps, 'remove'> // (index: number) => void
}
Where the second object is optional. The above is not working, as it is not expecting isSettings in the 3rd object. But it is always required.
Standard object types in Flowtype are objects defined to have at least the properties specified. This means that if you have a type like { isSettings: boolean } you are saying only that that the object has an isSettings property that is a boolean. It is allowed to have other properties, it just has to know the type of isSettings.
This means that if you have a type
type Props = {
isSettings: boolean,
} | {
index: number,
remove: (index: number) => void
};
then doing
var obj: Props = ...
if (obj.remove) {
var n: number = obj.index;
}
will fail, because it doesn't prove anything, because you have not prohibited there being a remove property on both objects.
In order to refine an object type like this one, Flow needs to be told that each type has exactly the given set of properties. This is where Flow's Exact object types come in.
If you change your types to be
type Props = {|
isSettings: boolean,
|} | {|
index: number,
remove: (index: number) => void
|};
then a snippet like
var obj: Props = ...
if (obj.remove) {
var n: number = obj.index;
}
will work as expected, because the presence of remove means there must be a property called index that is a number.

Why doesn't Flow see members of objects in this polymorphic union type?

The Setup
Here's a complete try flow example illustrating the issue.
Types
export type ActionT<TT: string, PT> = {|
+type: TT,
+payload: PT,
+error?: boolean,
+meta?: any
|}
export type ChangePayloadT = {
+_change: {|
+state: 'PENDING' | 'FULFILLED' | 'REJECTED',
+id: string,
+error?: any,
+message?: string,
|}
}
export type IdPayloadT = {
id: string,
}
type PayloadT = IdPayloadT | ChangePayloadT
type MyActionT = ActionT<'SET' | 'MERGE', PayloadT>
As you can see, MyActionT can contain a payload with either an id or a _change object. It's not quite (?) a disjoint union because there isn't a single property to disambiguate on.
This seems like it should work, but doesn't:
function lookup3 (action: MyActionT): any {
if (action.payload._change) {
// why does this error?
return action.payload._change.id
} else {
return action.payload.id
}
}
Anyone care to set me straight as to why?
Ok, so the solution apparently involved making the the two types a proper disjoint union:
export type ChangePayloadT = {
+_change: {|
+state: $Keys<typeof asyncStates>,
+id: string,
+error?: any,
+message?: string,
|},
id?: string,
}
export type IdPayloadT = {
+_change?: void,
+id: string,
}
With the second type now having an explicitly void _change, flow knows to tell the types apart based on the presence or absence of a _change.
Working tryflow Yay. :)

Resources