'Only allow specific components as children in React and Typescript

I would like to only allow specific components as children. For example, let's say I have a Menu component, that should only contain MenuItem as children, like this:

<Menu>
  <MenuItem />
  <MenuItem />
</Menu>

So I would like Typescript to throw me an error in the IDE when I try to put another component as child. Something warning me that I should only use MenuItem as children. For example in this situation:

<Menu>
  <div>My item</div>
  <div>My item</div>
</Menu>

This thread is almost similar but does not include a TypeScript solution. I was wondering if the problem can be solved using TypeScript types and interfaces. In my imaginary world it would look like this, but of course the type checking is not working because the child component has an Element type:

type MenuItemType = typeof MenuItem;

interface IMenu {
  children: MenuItemType[];
}

const MenuItem: React.FunctionComponent<IMenuItem> = ({ props }) => {
  return (...)
}

const Menu: React.FunctionComponent<IMenu> = ({ props }) => {
  return (
    <nav>
      {props.children}
    </nav>
  )
}

const App: React.FunctionComponent<IApp> = ({ props }) => {
  return (
    <Menu>
      <MenuItem />
      <MenuItem />
    </Menu>
  )
}

Is there a way to achieve this with Typescript? Like to extend the Element type with something related only to a specific component?

Or what would be a good approach for being sure that a child is an instance of a specific component? Without having to add condition that looks at the child component displayName.



Solution 1:[1]

To do that you need to extract props interface from children component (and preferably also parent) and use it this way:

interface ParentProps {
    children: ReactElement<ChildrenProps> | Array<ReactElement<ChildrenProps>>;
}

so in your case it would look like this:

interface IMenu {
  children: ReactElement<IMenuItem> | Array<ReactElement<IMenuItem>>;
}

const MenuItem: React.FunctionComponent<IMenuItem> = ({ props }) => {
  return (...)
}

const Menu: React.FunctionComponent<IMenu> = ({ props }) => {
  return (
    <nav>
      {props.children}
    </nav>
  )
}

Solution 2:[2]

Despite the answer above, you can't do this with children. You might do a runtime check in a dev build of your component, but you can't do this with TypeScript types — at least, not yet.

From TypeScript issue #18357:

Right now there's no way to specify what children representation is, except specifying ElementChildrenAttribute inside JSX namespace. It's heavily coupled with React representation for children which implies that children is a part of props. This makes impossible to enable type checking for children with implementations which store children separately, for instance https://github.com/dfilatov/vidom/wiki/Component-properties.

And note that it's referenced in #21699, where basically the possibly-breaking change around ReactElement may also make it possible to do this.

Right now, all you can do is that runtime check, or accept props (or arrays of props) and optionally a component function (in your case, you know it's MenuItem) and create the elements within your component.

There's also the question of whether you should do this. Why shouldn't I be able to write a component that returns a MenuItem rather than having to use MenuItem directly? :-)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Minwork
Solution 2 T.J. Crowder