Flow: How to check the specific type of (React.Element<A>|React.Element<B>)

Created on 24 Jan 2018  ยท  9Comments  ยท  Source: facebook/flow

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>

flow try

Most helpful comment

@apsavin Because that is not how jsx works. <A /> does not return an instance of A, and instanceof 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

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}`)

All 9 comments

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.

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

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]

try

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:
image

I ended creating an assert function that returns the proper type with a couple of checkings and throws here and there

Was this page helpful?
0 / 5 - 0 ratings

Related issues

davidpelaez picture davidpelaez  ยท  3Comments

ctrlplusb picture ctrlplusb  ยท  3Comments

philikon picture philikon  ยท  3Comments

ghost picture ghost  ยท  3Comments

Beingbook picture Beingbook  ยท  3Comments