How to Type the React children Prop in TypeScript

5 min read.
Tags: ReactTypeScript

To type the React children prop in TypeScript, declare it yourself as React.ReactNode. Since React 18, components don't get children for free, so you have to write it on the props type.

Quick answer

import type { ReactNode } from 'react'

type CardProps = {
  title: string
  children: ReactNode
}

function Card({ title, children }: CardProps) {
  return (
    <section>
      <h2>{title}</h2>
      {children}
    </section>
  )
}

ReactNode covers everything React can render: JSX elements, strings, numbers, null, undefined, booleans, and arrays of those.

<Card title="Mixed">
  text {1} {null} {false} {[<b key="1" />]} <i />
</Card>

All of that compiles.

Make it children?: ReactNode if the component also works with nothing inside, like <Panel />.

React.FC no longer adds children

Older tutorials write this:

const Layout: React.FC = ({ children }) => <main>{children}</main>

With React 18 or 19 types, it fails:

error TS2339: Property 'children' does not exist on type '{}'.

React.FC used to add children to every component's props, even ones that never rendered them. The React 18 types removed that. Add it explicitly:

const Layout: React.FC<{ children?: ReactNode }> = ({ children }) => (
  <main>{children}</main>
)

Or skip React.FC and type the props parameter directly, like the Card example above. Both compile the same.

PropsWithChildren for the lazy version

React ships a helper that adds an optional children?: ReactNode to any props type:

import type { PropsWithChildren } from 'react'

function Box({ padding, children }: PropsWithChildren<{ padding: number }>) {
  return <div style={{ padding }}>{children}</div>
}

Children are optional here, so <Box padding={8} /> compiles too. If the component needs children, write children: ReactNode yourself instead.

Why not JSX.Element?

JSX.Element (or React.JSX.Element in React 19 types) means exactly one JSX element. Text fails:

type Props = { children: React.JSX.Element }

<Wrapper>plain text</Wrapper>
error TS2747: 'Wrapper' components don't accept text as child elements. Text in JSX has the type 'string', but the expected type of 'children' is 'Element'.

So do two elements:

error TS2746: This JSX tag's 'children' prop expects a single child of type 'Element', but multiple children were provided.

With @types/react 19, the bare global JSX.Element is gone and gives Cannot find namespace 'JSX'. Use React.JSX.Element if you really want one element. For a normal wrapper, ReactNode is the right type.

When you need exactly one element

Some components clone their child to add props, like a tooltip that sets title. Type the child as ReactElement with the props you're going to set:

import { cloneElement, type ReactElement } from 'react'

type TooltipProps = {
  text: string
  children: ReactElement<{ title?: string }>
}

function Tooltip({ text, children }: TooltipProps) {
  return cloneElement(children, { title: text })
}

<Tooltip text="Save changes">
  <button>Save</button>
</Tooltip>

Children as a function

For the render-prop pattern, type children as a function that returns ReactNode:

type MouseProps = {
  children: (pos: { x: number; y: number }) => ReactNode
}

function Mouse({ children }: MouseProps) {
  const pos = { x: 0, y: 0 } // track the real position here
  return <div>{children(pos)}</div>
}

<Mouse>{({ x, y }) => <p>{x}, {y}</p>}</Mouse>

TypeScript infers x and y inside the callback from the props type.

You can't restrict children to one component type

A common try is limiting <Tabs> to <Tab> children:

type TabsProps = {
  children: ReactElement<TabProps> | ReactElement<TabProps>[]
}

<Tabs>
  <div>not a tab</div>
</Tabs>

This compiles without an error. Every JSX expression has the same type, React.JSX.Element, whatever component it renders, so TypeScript can't tell a <div> from a <Tab>. If you need that guarantee, check at runtime (child.type === Tab), or take the data as a prop instead, like tabs={[{ label: 'One', content: ... }]}.

Common mistakes

Using React.FC and expecting children. It hasn't been implicit since React 18. Declare it.

Typing children as JSX.Element. It rejects text and multiple children. Use ReactNode.

Typing children as string. Then <Button><Icon /> Save</Button> fails. Use ReactNode unless the component really only takes text.

Rendering a plain object. ReactNode doesn't include objects, so <Card>{{ a: 1 }}</Card> is a type error, which saves you from React's "Objects are not valid as a React child" crash at runtime.

Conclusion

Type children as React.ReactNode, written out on your props type, and make it optional only when the component works empty. Use ReactElement when you clone a single child, a function type for render props, and PropsWithChildren when optional children are fine.