DEV Community

Cover image for Unsafe 'PropsWithChildren' Utility type in React TypeScript App
Muhammad A Faishal
Muhammad A Faishal

Posted on • Edited on

Unsafe 'PropsWithChildren' Utility type in React TypeScript App

As React developer, we are all familiar with creating components, whether as classes or functions, to add features to web pages.

One of the common cases in React is a component that has JSX Children. When you want to nest content inside a JSX tag, the parent component will receive that content in a prop called children.

For instance, the Modal component below will receive children prop set to and render it in a wrapper div.

// React JavaScript

function Modal(props) {
  const { children } = props;

  return <div className="modal">{children}</div>;
}

function Page() {
  return (
    <Modal>
      <title>Submit Form</title>
      <form onSubmit={handleSubmit}>
        <input type="text" />
        <input type="submit" />
      </form>
    </Modal>
  );
}
Enter fullscreen mode Exit fullscreen mode

If it wants to be converted to TypeScript, most of developers use PropsWithChildren utility type from @types/react package.

// React TypeScript

import type { PropsWithChildren } from "react";

// props: {
//    children?: React.ReactNode;
// }
function Modal(props: PropsWithChildren<{}>) {
  const { children } = props;

  return <div className="modal">{children}</div>;
}

function Page() {
  return (
    <Modal>
      <title>Submit Form</title>
      <form onSubmit={handleSubmit}>
        <input type="text" />
        <input type="submit" />
      </form>
    </Modal>
  );
}
Enter fullscreen mode Exit fullscreen mode

It helps TypeScript understand that the component should have the children prop.

Problems

Sounds good so far, right? But here's where things get weird.

Let me highlight the code.

// props: {
//    children?: React.ReactNode; ❌
// }
Enter fullscreen mode Exit fullscreen mode
// Unexpected ❌

import type { PropsWithChildren } from "react";

// props: {
//    children?: React.ReactNode;
// }
function Modal(props: PropsWithChildren<{}>) {
  const { children } = props;

  return <div className="card">{children}</div>;
}

function Page() {
  return <Modal />;
}
Enter fullscreen mode Exit fullscreen mode

The children is supposed to be required. That means we must provide children when using Modal component. But, guess what? It becomes optional! Rendering Modal component without children is now even possible 😨.

Let's check what's inside PropsWithChildren. Here it is referring to @types/react Github.

type PropsWithChildren<P = unknown> = P & { children?: ReactNode | undefined };
Enter fullscreen mode Exit fullscreen mode

Turns out the culprit is @types/react itself 😡.

This little bug might not seem like a big deal at first, but trust me, it can cause some serious problems and mess up with your business logic. And the worst part? Neither React nor TypeScript will show any errors, leaving you scratching your head when things go wrong πŸ˜΅β€πŸ’«.

Solutions

So, what can we do about this? Let's explore some solutions!

1. Never Trust and Always Check "third-party" code

As you know earlier, the culprit is @types/react itself. This is not the first time. Previously, developers had a problem with React.FC. Some of them even posted about it.

TypeScript + React: Why I don't use React.FC
Why you probably shouldn’t use React.FC to type your React components
Why You Should Probably Think Twice About Using React.FC

All those fancy "magic" codes will eventually lose their "magic" over time. So, you should always check what's behind the code.

2. Create your own Utility type

Since the PropsWithChildren from @types/react is not that hard, why don't you create it yourself?

type PropsWithChildren<P = unknown> = P & { children: ReactNode };
Enter fullscreen mode Exit fullscreen mode

PropsWithChildren utility type is for components that have children prop and it's required.

type PropsWithOptionalChildren<P = unknown> = P & { children?: ReactNode };
Enter fullscreen mode Exit fullscreen mode

PropsWithOptionalChildren utility type is for components that have children prop, but it's optional.

It looks reliable and unambiguous. βœ…

Conclusion

Remember these tips and you'll save yourself from headaches. I hope these tips come in handy for your projects! ✨

Top comments (10)

Collapse
 
stretch0 profile image
Andrew McCallum

That's a veey interesting point. It seems it's quite finicky to get the utility prop for react children right.

Would be worth raising this in the react GitHub to get the React team's thoughts on it

Collapse
 
maafaishal profile image
Muhammad A Faishal

Yeah, it would be. Gonna give it a try

Collapse
 
dagropp profile image
Daniel Gropp • Edited

IMO it’s not correct that the children should always be required. Prior to react 18 (the latest version), all components were in fact β€œPropsWithChildren”, and the children were optional. In real life (outside of TypeScript) this is still the case- you can always pass/unpass children, only TypeScript will show an error.
The PropsWithChildren is only a mean to mimick the old react functionally more easily. It’s definitely a use case for children to be optional.
If you want children to be required or be of specific type than just provide the children prop yourself as it serves you.

Collapse
 
maafaishal profile image
Muhammad A Faishal

Yeah, there is no issue regarding the reason behind creating PropsWithChildren.

Therefore, I just wanted to make it clear to developers who frequently use PropsWithChildren about a potential issue and suggested a couple of solutions as I mentioned above.

Collapse
 
albertgao profile image
AlbertGao • Edited

Incorrect comment. How does React handles the children and how your business logic handles the children are 2 completely different things. OP is discussing the latter case, thus totally fine.

Also. Some of the types in the @types/react exists only because removing them would cause breaking change. Nowadays, I tend to just declare the children for each component, be it a number, string, ReactElement, make it specific.

Collapse
 
advaitju profile image
Advait Junnarkar • Edited

This seems sound, but is unnecessary. The reason is that these two are equivalent:

type PropsWithChildren<P = unknown> = { children?: ReactNode | undefined };
type PropsWithRequiredChildren<P = unknown> = { children: ReactNode };
Enter fullscreen mode Exit fullscreen mode

They're the same because ReactNode is a union where one possible value is undefined. Removing the optional prop ? doesn't change it.

You'll see in this TypeScript Playground example both of these types have the same children type in a component.

Collapse
 
maafaishal profile image
Muhammad A Faishal

It seems equivalent when you focus on the declaration. You will find the big difference when you call both components within the parent components.

I've added some codes on this playground to make it easy to understand. You can take a look.

Collapse
 
sommereder profile image
Mario Sommereder

Why not use Required<PropsWithChildren>?

Collapse
 
maafaishal profile image
Muhammad A Faishal • Edited

Yeah, it can be an option, but it's suitable only for certain cases. Please check the unexpected case as follow:

Required<PropsWithChildren<{ title?: string }>>
// Result: { title: string; children: ReactNode; }
Enter fullscreen mode Exit fullscreen mode

children becomes required, but in other case title becomes required too.

You can check the code on https://www.typescriptlang.org/play/?ssl=7&ssc=69&pln=7&pc=22#code/JYWwDg9gTgLgBAbzgJQKYEMDGMByEAmqcAvnAGZQQhwDkUG2NAsAFCswCeYRACpWAGcA6sBgALAMJjgAG3z0AdgB4ecALxwArgoDWCiAHcFAPnVxVAMkRxM0uYoD8ALhQNcBIgB8tCwmWAKqPgkANys4Syc3HAA8lDAAOYB6DJ8EIJmaYIi4lKy8qjKSDCiMqjOcAIw8QoJJMbsXERoAI6awPT4WQJmre2dKvzCopJ2BUVwJTBlFVU1dcTGDWwsQA

Collapse
 
sommereder profile image
Mario Sommereder

This isn't an unexpected case. This is how Required works. If you do not want title to be optional, you have to move it out of Required:

type WhatYouWantTo = { title?: string } & Required<PropsWithChildren>

Enter fullscreen mode Exit fullscreen mode

Wouldn't that be the correct way?