1

I'm building a component that requires at least two specific child components to be used.

These two components are exported as dot notations from the main component and include defaultProps to identify them in the main component:

export interface ComponentPropsInterface {
  children: React.ReactChild;
}

const Component = ({ children }: ComponentPropsInterface ): JSX.Element => {
  const childrenArray = React.Children.toArray(children);
  return (
    <>
        {
          // Find a component with the wanted prop and place it in a specific place
          childrenArray.filter(
            (child) =>
              child.props.__TYPE === 'Trigger'
          )[0]
        }
    </>
  );
};

const Trigger = ({
  children,
}: {
  children: React.ReactChild;
  __TYPE?: string;
}) => <>{children}</>;

// Prop is assigned here automatically
Trigger.defaultProps = {
  __TYPE: 'Trigger',
};

// Export as dot notation
Popover.Trigger = Trigger;

// Export component
export default Component;

In the above I'm checking if Component has children, which is okay, but it's not failproof.

I would like to ensure children has a at least one child with the __TYPE === 'Trigger' prop.

Is there any way to do this directly from Typescript? Or should I throw an error if the child component I need isn't present?

EDIT: For precision's sake, I'd like the error to appear in the IDE on the parent <Component/> when it is used without one of the required children.

lpetrucci
  • 1,285
  • 4
  • 22
  • 40
  • 1
    I'm not going to answer since I haven't checked this for a few months, and it might have changed, but it used to be the case that this was not possible. This question gives the method that 'should' work: https://stackoverflow.com/questions/57627929/only-allow-specific-components-as-children-in-react-and-typescript but the typescript compiler does not have the ability do differentiate between JSX Elements. – David Hall Sep 09 '21 at 10:09

1 Answers1

3

As a rule of thumb, if you are using typescript and you want to use Array.prototype.filter - use custom typeguard as a filter predicate:

import React from 'react'

export interface ComponentPropsInterface {
  children: React.ReactElement<{ __TYPE: string }>[];
}

const predicate = (child: React.ReactChild | React.ReactFragment | React.ReactPortal):
  child is React.ReactElement<{ __TYPE: 'Trigger' }> =>
  React.isValidElement(child) ? child.props.__TYPE === 'Trigger' : false

const Component = ({ children }: ComponentPropsInterface): JSX.Element => (
  <>
    {
      // Find a component with the wanted prop and place it in a specific place
      React.Children.toArray(children).filter(predicate)[0]
    }
  </>
)

const Trigger = (props: {
  __TYPE: string;
}) => <div></div>;

const Invalid: React.FC<{ label: string }> = ({ label }) => {
  return <div>{label}</div>;
};


const Test = () =>
  React.createElement(Component, {
    children: [
      React.createElement(Invalid, { label: "d" }), // error
      React.createElement(Trigger, { __TYPE: 'Trigger' }), // ok
    ],
  })

Playground Also, in order to get props you need to check if element is valid React.isValidElement

AFAIK, it is possible to infer type of child components only with native react syntax createElement, because this method has a lot of overloads. jsx syntax, from the other side, infers every element to JSX.Element

Please see my article

  • Thank you for improving my code! This is a lot better logic wise, but it still doesn't throw an error if the required child isn't used. – lpetrucci Sep 09 '21 at 10:20
  • I have added a fallbck. I don't think it is good idea to throw any error inside react component – captain-yossarian from Ukraine Sep 09 '21 at 10:25
  • I tested https://stackoverflow.com/a/57628053/8685682 and adding `children: Array>` to check the type of the child seems to work. If you want to add it to your answer I can mark it as accepted – lpetrucci Sep 09 '21 at 10:29
  • @Eight just curious, does my solution work? Because I did not test it in runtime . Since you already added a link to example which is works for you I don't think it worth adding to my answer. Feel free to not accept my answer. Just don't want to duplicate anything – captain-yossarian from Ukraine Sep 09 '21 at 10:36
  • Your answer isn't exactly the answer to the question as it just makes the `filter` more precise. The link actually throws a typescript error unless a child of type is passed which is closer to the initial question. Up to you honestly, I can self answer if you prefer. – lpetrucci Sep 09 '21 at 10:41
  • SUre, maybe I just did not understand the question. No problem. Please answer, I will upvote your answer – captain-yossarian from Ukraine Sep 09 '21 at 10:42
  • Upon further inspection the other answer didn't work Must've done something wrong. Back to square one! – lpetrucci Sep 09 '21 at 10:58
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/236940/discussion-between-captain-yossarian-and-eight). – captain-yossarian from Ukraine Sep 09 '21 at 11:30