How can I check the actual type of a component passed in to render()
while making flow happy?
Basically, I want to make something like the HTML <table>
, where the visual order of <thead>
, <tbody>
and <tfoot>
are always all <thead>
first, then all <tbody>
and finally all <tfoot>
, regardless of the order they were added to the DOM. Similarly, I might want something like <tr>
being lumped into a <tbody>
as a convenience method for the dev.
The problem is that at runtime, it seems like testing <Type/>.type === Type
works just fine, but flow does not accept that we have made the type-check.
Following is an example that shows the basic idea I have of determining the type. It works fine at runtime for React 15 and 16, but it fails flow. Is it possible to write the code in such a way that it works everywhere?
// @flow
import * as React from 'react'
class A extends React.Component<{ a: string }> { render() { return <div>A: {this.props.a}</div> } }
class B extends React.Component<{ b: string }> { render() { return <div>B: {this.props.b}</div> } }
class CA extends React.Component<{ children: React.Element<typeof A> }> {
render() {
const { children } = this.props
return <div>{children.props.a}</div>
}
}
class CB extends React.Component<{ children: React.Element<typeof B> }> {
render() {
const { children } = this.props
return <div>{children.props.b}</div>
}
}
class C extends React.Component<{ children: React.Element<typeof A>|React.Element<typeof B> }> {
render() {
const { children } = this.props
if(children.type === A) {
// The next line errors because flow thinks that `children` might be of type B
return <CA>{children}</CA>
}
if(children.type === B) {
// The next line errors because flow thinks that `children` might be of type A
return <CB>{children}</CB>
}
}
}
<C><A a="bum" /></C>
Why not use instanceof
? See example.
@apsavin Because that is not how jsx works. <A />
does not return an instance of A
, and instanceof
will therefore always return false.
Or go to https://reactjs.org and paste the following into one of the live-try boxes so see it yourself. You need to add a whitespace after pasting, to trigger the live-code runner
class A extends React.Component {
render() {
}
}
alert(`new+instanceof: ${new A instanceof A}.
jsx+instanceof: ${<A/> instanceof A}.
new+type: ${(new A).type === A}.
jsx+type: ${<A/>.type === A}`)
Did you find a way ?
Looking for same. Any results?
Also would like to know
I might have a solution. Currently, I'm using this function:
// @flow
import * as React from 'react'
export default function filterComponentsByType<P, T:React.ComponentType<P>>(elements:React.ChildrenArray<?false|React.Element<*>>, type: T) : $ReadOnlyArray<React.Element<T>> {
const asArray = React.Children.toArray(elements)
const nullOrCorrectType = asArray.map(child => {
if(!child) { return null }
return child.type === type ? child : null
})
const filtered = nullOrCorrectType.filter(Boolean)
return filtered
}
You give it this.props.children
and a type, and it will return the children that match that type. It could easily be extended to also return the children that does not match the type.
Try Flow proving the typing works
Code for reactjs.org live box (still, you need to type anything after pasting to trigger the parser)
function filterComponentsByType<P, T:React.ComponentType<P>>(elements:React.ChildrenArray<?false|React.Element<*>>, type: T) : $ReadOnlyArray<React.Element<T>> {
const asArray = React.Children.toArray(elements)
const nullOrCorrectType = asArray.map(child => {
if(!child) { return null }
return child.type === type ? child : null
})
const filtered = nullOrCorrectType.filter(Boolean)
return filtered
}
class A extends React.Component<{}> {
render() {
return <div>This is an A</div>
}
}
function B(props:{}) {
return <div>This is a B</div>
}
class C extends React.Component<{ children: React.ChildrenArray<React.Element<typeof A>|React.Element<typeof B>> }> {
render() {
const a = filterComponentsByType(this.props.children, A)
const b = filterComponentsByType(this.props.children, B)
return <div>
<h1>A</h1>
{a}
<h1>B</h1>
{b}
</div>
}
}
ReactDOM.render(<C>
<A />
<B />
{null && <A/>}
{false && <B/>}
<A />
<B />
</C>, mountNode)
Still trying to achieve this, but no luck so far.
/* @flow */
import React from 'react';
import type { Element } from 'react';
class A extends React.Component<{ a: string }> {}
class B extends React.Component<{ b: string }> {}
type AB = Element<typeof A> | Element<typeof B>;
const getAorB = (component: AB): string => {
if (component.type === A) {
return component.props.a;
}
if (component.type === B) {
return component.props.b;
}
return "";
}
13: return component.props.a;
^ Cannot get `component.props.a` because property `a` is missing in object type [1].
References:
7: class B extends React.Component<{ b: string }> {}
^ [1]
17: return component.props.b;
^ Cannot get `component.props.b` because property `b` is missing in object type [1].
References:
6: class A extends React.Component<{ a: string }> {}
^ [1]
I think I have a very similar case, but I am using cloneElement, whic gives me a rather cryptic error:
Error โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ ../spring/src/components/DescriptionText/DescriptionText.js:162:11
Cannot call cloneElement because property variant is missing in call of cloneElement [1] but exists in props [2].
159โ title
160โ ) : (
161โ // So if it is not string it must be one of the compatible Text types, therefore not wrap it
[1][2] 162โ cloneElement(title, { variant: selectedVariant.title.variant })
And here it is nicely colored:
I ended creating an assert function that returns the proper type with a couple of checkings and throws here and there
Most helpful comment
@apsavin Because that is not how jsx works.
<A />
does not return an instance ofA
, andinstanceof
will therefore always return false.See flow fail the check
Or go to https://reactjs.org and paste the following into one of the live-try boxes so see it yourself. You need to add a whitespace after pasting, to trigger the live-code runner