Skip to content
This repository has been archived by the owner on Sep 1, 2024. It is now read-only.

Document best practice for using self-referencing PropTypes #316

Open
posita opened this issue Jun 22, 2020 · 2 comments
Open

Document best practice for using self-referencing PropTypes #316

posita opened this issue Jun 22, 2020 · 2 comments

Comments

@posita
Copy link

posita commented Jun 22, 2020

This comment claims this use case is pretty rare, but I'm either ignorant of the alternatives, or I'm not convinced. Anywhere one wants to have a tree-like component structure, one potentially butts up against this limitation. Consider a structure involving Nodes and Leafs as follows:

function LeafComponent({ id, name }: LeafComponentT) {
  return (
    <li>{name}</li>
  )
}

LeafComponent.propTypes = {
  id: PropTypes.string.isRequired,
  name: PropTypes.string.isRequired,
}

type LeafComponentT = InferProps<typeof LeafComponent.propTypes>

NodeComponent.propTypes = {
  leaves: PropTypes.arrayOf(
    PropTypes.shape(LeafComponent.propTypes).isRequired
  ).isRequired,
}

type NodeComponentT = InferProps<typeof NodeComponent.propTypes>

function NodeComponent({ leaves }: NodeComponentT) {
  return (
    <ul>
      {leaves.map((leaf, _) => (
        <LeafComponent key={leaf.id} {...leaf} />
      ))}
    </ul>
  )
}

So far, a Node is merely a list of Leafs. Now let's say we want to augment our Node to contain zero or more entries, each of which may either be a Node or Leaf. How is this done? This won't work:


function recursive(...args: any) {  // 7023: 'recursive' implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.
  return PropTypes.arrayOf(
    PropTypes.oneOfType([
      LeafComponent.propTypes,
      PropTypes.shape({  // 2615: Type of property 'children' circularly references itself in mapped type '{ children: never; }'.
        children: recursive,
      }).isRequired,
    ])
  )(...args)  // 2556: Expected 5 arguments, but got 0 or more.
}


function NodeComponent({ children }: NodeComponentT) {
  return (<ul>{
    children.map((child, _) => {
      if (child instanceof Object
        && "children" in child) {
        const node_child = child as NodeComponentT
        return (<li><NodeComponent children={node_child.children} /></li>)
      } else {
        const leaf_child = child as LeafComponentT
        return (<LeafComponent key={leaf_child.id} {...leaf_child} />)
      }
    })
  }</ul>)
}

NodeComponent.propTypes = {
  children: PropTypes.arrayOf(recursive).isRequired,
}

type NodeComponentT = InferProps<typeof NodeComponent.propTypes>

You'll get a circular reference type error. So how should one handle this case in the real world? I can find near zero documentation on how to do this, either here or via the React docs. Any guidance would be appreciated.

@ljharb
Copy link
Contributor

ljharb commented Jun 22, 2020

I agree that circular propTypes are needed for a tree-like structure that allows circularity, but that specific need is indeed exceedingly rare.

You'd handle this by using a custom propType to wrap the recursion, rather than actually using recursion.

@posita
Copy link
Author

posita commented Jun 22, 2020

To clarify, what I'm pointing out by filing this issue is that no clear guidance exists to address this pattern. I've clearly gotten it wrong here, but that's largely because I've been feeling around in the dark absent such guidance.

You'd handle this by using a custom propType to wrap the recursion, rather than actually using recursion.

☝️ Thanks, but I don't understand what that means or whether it differs from your prior recommendation on #246. Can you point to a clear example?


Right now, I've constructed the following work-around:

type LeafComponentT = InferProps<typeof LeafComponent.propTypes>

function LeafComponent({ id, name }: LeafComponentT) {
  return (
    <span>{name} ({id})</span>
  )
}

LeafComponent.propTypes = {
  id: PropTypes.string.isRequired,
  name: PropTypes.string.isRequired,
}

interface RootComponentT {
  children: NodeComponentT[],
}

function RootComponent({ children }: RootComponentT) {
  if (children
    && children.length > 0) {
    return (
      <ul>
        {children.map((child, _) => (
          <NodeComponent {...child} />
        ))}
      </ul>
    )
  } else {
    return null
  }
}

RootComponent.propTypes = {
  children: PropTypes.array.isRequired,
}

type NodeComponentT = RootComponentT & {
  leaf: InferProps<typeof LeafComponent.propTypes>,
}

function NodeComponent({ leaf, children }: NodeComponentT) {
  return (
    <li>
      <LeafComponent key={leaf.id} {...leaf} />
      <RootComponent children={children} />
    </li>
  )
}

NodeComponent.propTypes = {
  leaf: PropTypes.shape(LeafComponent.propTypes).isRequired,
  children: PropTypes.array.isRequired,
}

I call this a work-around because there is no runtime type checking of the elements of children.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants