Reputation: 600
I'm trying this syntax using .reduce
:
function arrToObject<T: {key: string}, R: {[string]: T}>(list: Array<T>): R {
return list.reduce((result: R, item: T): R => {
result[item.key] = item;
return result;
}, {});
}
But Flow gives the following error:
call of method `reduce`. Function cannot be called on any member of intersection type
Upvotes: 1
Views: 146
Reputation: 4231
@PeterHall's answer identifies the reason for the failed typechecking correctly, but the solution is not optimal. As the answer says, the problem is that R
may be any type satisfying {[string]: T}
, but what's required is actually that type exactly. In contrast, a generic type parameter is needed to preserve item types when items go into and out of the arrToObject
function, with {key: string}
being the minimum signature required of the inputs.
This signature works:
function arrToObject<T: {key: string}>(list: Array<T>): {[string]: T}
Or, with named types:
type KeyMap<T> = {[string]: T};
type Item = {key: string};
function arrToObject<T: Item>(list: Array<T>): KeyMap<T>
Upvotes: 0
Reputation: 58725
The root problem is in reconciling the generic type R
with {}
, the initial value of the accumulator.
A hacky fix would to prevent Flow from trying to reconcile the types at all:
function arrToObject<T: {key: string}, R: {[string]: T}>(list: Array<T>): R {
var accum: any = {};
return list.reduce((result: R, item: T): R => {
result[item.key] = item;
return result;
}, accum);
}
But there are problems with that, as we'll find out further down. Things gets a little clearer when we explicitly say what type the accumulator is:
function arrToObject<T: {key: string}, R: {[string]: T}>(list: Array<T>): R {
let accum: R = {};
return list.reduce((result: R, item: T): R => {
result[item.key] = item;
return result;
}, accum);
}
This forces the problematic type reconciliation into a simpler statement without so much noise around it. And it produces a much better error:
3: let accum: R = {};
^ object literal. This type is incompatible with
3: let accum: R = {};
^ some incompatible instantiation of `R`
This error doesn't tell the whole story, but it brings us closer to what is really going on.
Flow requires that the generic parameter R
can be any type compatible with the constraint. Valid instantiations of R
could therefore be more general than the constraints you put on it, for example having additional required fields.
This creates a problem. Inside the function body, you can't possibly know what the actual instantiation of R
looks like. So you can't construct one. Even though the error messages are sucky, Flow is correct to stop you from doing this! What if someone called your function with an R
instantiated to be something with an extra field, for example {[string]: {key: string}, selected: boolean}
? Then you would have to have somehow known to initialise the accumulator with a selected: boolean
field.
If you are able to, a much better solution than the hack above is to remove the generics altogether:
type KeyMap = {[string]: {key: string}};
type Item = {key: string};
function arrToObject(list: Array<Item>): KeyMap {
return list.reduce((result:KeyMap, item: Item): KeyMap => {
result[item.key] = item;
return result;
}, {});
}
I also introduced some type aliases to prevent the function signature getting out of control.
Now the generics are replaced with concrete types, things are much simpler. The body of the function can create a new initial value for the accumulator because its exact type is known.
If you really need the generic type parameters for some reason, you can also work around this by passing the accumulator's initial value as another argument to arrToObject
. That will work because the caller of your arrToObject
will know the concrete type and be able to instantiate it.
Upvotes: 2