45

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.

neiya
  • 2,657
  • 4
  • 23
  • 32
  • 1
    What if the children components extend a type rather than component, where that type extends component plus an extra attribute? – Amir-Mousavi Aug 23 '19 at 14:15

2 Answers2

29

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>
  )
}
Minwork
  • 838
  • 8
  • 9
  • 3
    Great solution it works well! And is it possible to check that it contains _only_ children of these types? For example, if there is a MenuItem component and a div tag, no error is showing. – neiya Aug 23 '19 at 14:24
  • I think it is not possible in typescript (but I may be wrong) because it can't dynamically resolve that components nested inside some component should match type declared in that component children prop. – Minwork Aug 23 '19 at 14:52
  • Also from what I remember, even though it was technically possible using PropTypes in jsx, it required a nasty hack to extract component name (which may not always be there) and compare it to children components names. – Minwork Aug 23 '19 at 14:59
  • 12
    I actually don't know if this answer is correct. I don't know what checks these types on `children` add, but they only seem to ensure that a child exists, not that it is of a specific type. The problem boils down to the fact that anything that renders JSX returns `JSX.Element`, which is generic and not specific enough to compare. There are more details in this issue on the TypeScript github: https://github.com/microsoft/TypeScript/issues/21699 – Salem Apr 28 '21 at 13:16
  • 4
    I'm with @Salem on this one. What if you had a `RedButtonContainer` component that can only have `RedButton`s as children? If `RedButton` took the exact same props as `BlueButton`, they would have the same type (as far as TypeScript is concerned), even though the buttons show up as blue instead of red. `RedButton` and `BlueButton` would just be aliases for the same thing, right? – ChrisCrossCrash May 16 '21 at 14:35
  • You arę right @ChrisCrossCrash. In TS the best you can get is to distinguish components by props. As I said before it is a tricky thing to differentiate components from one another and gets even trickier in pure JS. – Minwork May 17 '21 at 18:53
  • This types fine, but doesn't seem to actually error out when I place invalid children in the company in JSX. E.g. I put a div in there, and no error. – Adam A Nov 05 '22 at 00:51
11

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? :-)

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875