How to properly type React.Children and React.cloneElement?

Viewed 6717

I'm cloning my react children with cloneElement but I'm having some problems to type them.

This is my code(it is just passing a custom onClick function as a prop to the children):

const childrenWithProps = React.Children.map(children, child =>
  React.cloneElement(child as ReactChild, {
    onClick: (e: React.FormEvent) =>
      childrenClickHandler(
        e,
        child.props.children,
        onItemClick && onItemClick(e),
      ),
  }),
);

And this is my ReactChild type:

export type ChildProps = {
  onClick?: (e?: React.ChangeEvent<{}>) => void;
  props: React.Props<ReactChild>;
  children: ReactElement | ReactElement[];
};
export type ReactChild = ReactElement<ChildProps> | ReactElement;

And typescript keeps saying that there is a problem with my props on child.props.children.

enter image description here

The message is:

Property 'props' does not exist on type 'ReactNode'. Property 'props' does not exist on type 'string'.

What I'm doing wrong?

2 Answers

The reason you're getting the message:

Property 'props' does not exist on type 'ReactNode'. Property 'props' does not exist on type 'string'.

Is because a ReactNode can be either a component, an html element, or a plain string. Strings don't accept props. I've found that all you have to do to clear it is properly type guard for a valid React Element:

const childrenWithProps = React.Children.map(children, child => {
    if (React.isValidElement(child)) {
        return React.cloneElement(child, {newProp: 'myNewProp'})
    }

    return child;
});

You could move/add the type to the argument of map callback since now it applies only to the first argument of React.cloneElement.

const childrenWithProps = React.Children.map(children, (child: ReactChild) => // <- Here
  React.cloneElement(child, { // <- Assertion here can be omitted
    onClick: (e: React.FormEvent) =>
      childrenClickHandler(
        e,
        child.props.children,
        onItemClick && onItemClick(e),
      ),
  }),
);

Assertion has the same scope as the variable, so

React.Children.map(children, child => ...)

without type makes child type any (if you do not specify type of children somehow)

Then child is used as an argument and depending on the type of the function and your tsconfig it may require a value of some type

React.cloneElement(child, ...)

You put assertion here but it affects the usage of child as argument. Actually it is possible to put the same assertion to the second usage:

  React.cloneElement(child as ReactChild, {
    onClick: (e: React.FormEvent) =>
      childrenClickHandler(
        e,
        (child as ReactChild).props.children, // <- Here
        onItemClick && onItemClick(e),
      ),
  }),

But in this case it is better to specify the type of the variable where it is defined so it would have the same type in all its scope.

Related