Reputation: 33
We use Flow with React, and we wrote this DropDown wrapper that does some things for us.
Until now, this drop down only needed to work with number
ids, but recently, we had to add support for string
ids as well.
Not wanting to rewrite everything, we thought about using a generic type parameter to our props, so that the ids (and the onChange handler) would use that generic type.
However, we do not seem to be able to use that change handler correctly. When trying to call the handler, we get a Flow error about the type of the value we are sending it (either number
or string
) telling us that this type is not compatible with T
:
Cannot call 'handler' with 'value' bound to 'value' because string [1] is incompatible with 'T'
And same thing with the number
call.
It was our belief that if we successfuly check the type of one of our generic values, it would collapse that type for every element using the same generic type in our props. Here they are:
type Props<T : string | number> = {
options?: Array<{id: T, displayValue: string}>,
onValueChange?: (value: T | null) => mixed,
}
Because we know that Flow will not allow to create an element using these props with an onValueChange
that would deal with number
if the options
contain ids with string
, for instance, we thought that by checking the type of an id in options
, we would collapse the T
for the onValueChange
as well.
Here is where we get the error:
export class DropDown<T : string | number> extends React.Component<Props<T>, {}> {
handleChange = (event: {target: {value: string}}) => {
if (!this.props.onValueChange) return
const handler = this.props.onValueChange
const firstElement = this.props.options && this.props.options[0]
if (!firstElement) return
const value = event.target.value
// I don't understand why, but this if-else if generates an error.
if (typeof firstElement.id === 'string') {
handler(value)
} else if (typeof firstElement.id === 'number') {
const num = parseInt(value)
handler(num)
}
}
}
As such, my question would be: is it possible to determine the generic type of the function's parameter so that we can use it as we want, without errors?
If the answer is yes, what are we doing wrong in our code that prevents the type check to work as wanted?
If the answer is no, is there a better way than casting our value as any
to make our handler accept it?
I have written a minimal working example in this Flow Try snippet, so you can see the live errors, as well as the expected usage (please note that the errors in the usage section are expected: they are there to show that using the generic component works as we want it to, even if using the values doesn't). The errors that matter are the first two in the error output.
Thank you in advance for your help!
Upvotes: 3
Views: 436
Reputation: 7666
Thanks for this very interesting question! Unfortunately I think that right now it is not possible to refine the type T by refining the type of the first element in the array. This can be seen when we use the following statement:
if (firstElement) {
if (typeof firstElement.id === 'string') {
const refined: string = firstElement.id;
const notRefined: T = refined; // Errors
const alwaysWorks: T = firstElement.id;
}
const alwaysWorks: T = firstElement.id;
}
The second assignment errors. Funnily this can be fixed by labeling also the first assignment as T
which shows how Flow works: Flow does not refine types itself, it refines the type label of a value. And I think you have rightfully already concluded that what you need to do is refine the value of the callback function. But this does not work since we cannot do any runtime checks on functions (except maybe get their number of arguments).
So we learn that Flow just isn't good enough. When you get deeper into using Flow you might notice this from time to time: It is just very hard to express what you are trying to do. I am not sure if Flow is at fault or if it is just JavaScript that is very hard to type. Anyways as a solution I want to suggest that you simply annotate the handler variable with Function
. Function is somewhat an any type and makes the function more general. We know that the code works (please see my corrections making the null check and then checking the id
property instead of the object) and the external interface is very explicit about the types. Sometimes it is the API that matters and internally we have to hack a bit. This is fine (we can for example see this a lot in popular projects like these 13 cases in graphql-js)
Bonus: If you are into functional programming check out how this library plays around with types to create the illusion of a Maybe type that has no runtime overhead since it is really just null
under the hood.
Upvotes: 1
Reputation: 1307
I think you are overthinking the problem.. Looks like you have just made a small mistake when declaring the type of event.target.value as a "string" but not "T".
Try this out
export class DropDown<T : string | number> extends React.Component<Props<T>, {}> {
handleChange = (event: {target: {value: T}}) => { // not {value: string}
if (!this.props.onValueChange) return
const handler = this.props.onValueChange
const value = event.target.value
handler(value)
}
}
I hope this will help you.
Upvotes: 0