Reputation: 9435
I find it hard to describe the problem, which might very well be why I can't find a solution. However I have a function that basically transforms and array of objects, to a map. Where certain (string) key is given as identifier.
Now I am trying to add typing to that function:
function mapFromObjects<K, T: {[string]: mixed}>(objects: $ReadOnlyArray<T>, key: string): Map<K, T> {
const m: Map<K, T> = new Map();
for (const item of objects) {
const keyval = item[key];
m.set(keyval, item);
}
return m;
}
Now this function gives the error
Cannot call `m.set` with `keyval` bound to `key` because mixed [1] is incompatible with `K` [2]
So I have to check/limit the generic type T. I tried doing this with:
function mapFromObjects<K, T: {[string]: K}>(objects: $ReadOnlyArray<T>, key: string): Map<K, T> {
const m: Map<K, T> = new Map();
for (const item of objects) {
const keyval = item[key];
m.set(keyval, item);
}
return m;
}
However if I now use this function as in:
type dataTy = {
id: number,
hidden: boolean,
}
const data_array: Array<dataTy> = [
{id: 0, hidden: true},
]
mapFromObjects(data_array, 'id');
The following error pops up:
Cannot call `mapFromObjects` with `data_array` bound to `objects` because number [1] is incompatible with boolean [2] in property `id` of array element.
Also this isn't unexexpected.
Now I "expect" both errors, however I have no way to "solve" it. The function works fine by itself, I just am stuck on how to correctly write the typing for the function. (Other than T: {[string]: any}
).
Upvotes: 1
Views: 474
Reputation: 1304
You want to use $ElementType
for this.
function mapFromObjects<T: {+[string]: mixed}, S: $Keys<T>>(
objects: $ReadOnlyArray<T>,
key: S,
): Map<$ElementType<T, S>, T> {
const m = new Map<$ElementType<T, S>, T>();
for (const item of objects) {
const keyval = item[key];
m.set(keyval, item);
}
return m;
}
type dataTy = {
id: number,
hidden: boolean,
}
const data_array: Array<dataTy> = [
{id: 0, hidden: true},
]
const res: Map<number, dataTy> = mapFromObjects(data_array, 'id');
// const res: Map<boolean, dataTy> = mapFromObjects(data_array, 'id'); // error
const keys: number[] = Array.from(res.keys());
//const keys: string[] = Array.from(res.keys()); // error
The first thing to know about this, is that if the 'id'
string in the example above is calculated in any way, this won't work. Like if you're doing some kind of string manipulation to get at the index string, you're out of luck.
The second thing to know is, if you want to get the type of an object property based on a string literal, you should use $PropertyType
. But if you want to get the type of an object property based on some arbitrary string type like a generic, you need $ElementType
.
One finicky thing I haven't entirely figured out yet is placing it in the return value vs in a generic. For example, this doesn't work:
function mapFromObjects<T: {+[string]: mixed}, S: $Keys<T>, K: $ElementType<T, S>>(
objects: $ReadOnlyArray<T>,
key: S,
): Map<K, T> {
const m = new Map<K, T>();
for (const item of objects) {
const keyval = item[key];
m.set(keyval, item);
}
return m;
}
It kinda feels like it should, but it does not. Not sure if this is something specific to flow or if I just don't know enough about type systems in general.
Upvotes: 1