Lesson 27 · Module 4.4 — Library-grade patterns

Reading React's renderable types — and the ComponentProps cheat

Three React types get mixed up constantly: ReactNode, ReactElement, and JSX.Element. They form a width ladder — ReactNode is "anything renderable", the other two are "a single element". Once you can place a type on that ladder you know what a prop will accept. Then one bonus: grabbing a component's props without anyone exporting a props type.

The reading move

Read these types by width. ReactNode = the widest set (strings, numbers, arrays, null, elements…). ReactElement / JSX.Element = the narrow "exactly one element" set. A children prop wants the wide one; a function that must return a single element wants the narrow one.

The width ladder

From the React TypeScript types (@types/react), simplified:

// widest: anything React can render in a slot
type ReactNode =
  ReactElement | string | number | boolean | null | undefined
  | ReactNode[] | ...;          // note: includes arrays and primitives

// narrow: ONE element object — { type, props, key }
interface ReactElement<P = any> { type: ...; props: P; key: ...; }

// JSX.Element is ReactElement with the default (any) generics — effectively the same
namespace JSX { interface Element extends ReactElement<any, any> {} }

So JSX.Element is what a piece of JSX like <div /> evaluates to — a ReactElement with loose generics. ReactNode is strictly wider: every ReactElement is a ReactNode, but a string or an array is a ReactNode and not a ReactElement.

Which to read where

you see…read it asaccepts
children: ReactNodea render slotanything renderable
(): ReactElementreturns one elementa single element only
icon: JSX.Elementone element propa single element only

The practical rule from this: type children as ReactNode (so callers can pass text, lists, null); reserve ReactElement/JSX.Element for slots that genuinely must be exactly one element. A common bug is typing children: JSX.Element and then being unable to pass a string.

A function component returns the narrow type

A component's return type is ReactElement (or JSX.Element), not ReactNode — historically it couldn't return a bare string or array at the top level. So props lean wide (ReactNode); return types lean narrow. Read the position to pick the expectation.

The bonus: ComponentProps<typeof X>

There's a real point worth making here: stop hand-exporting DashboardProps, MyTableProps and so on. If you need a component's props elsewhere, pull them straight off the component with the typeof trick — no export needed:

// instead of importing an exported props type:
type WrapperProps = ComponentProps<typeof ComponentA>;

// and if you don't have the component imported yet (e.g. in a hook):
import type { ComponentA } from './ComponentA';
type ComponentAProps = ComponentProps<typeof ComponentA>;

Read this as: typeof ComponentA gets the component's value type (its function type), and ComponentProps extracts the props from that. It "points directly at the component" rather than at some separately-exported type that can drift. As a rule, the only time you still name and export a props type is when you deliberately want a public, component-named contract — which is rare.

Check yourself

Recall, don't re-read. (Answers reveal on click.)

Which type should a children prop usually have, and why?
How do ReactElement and JSX.Element relate?
What does ComponentProps<typeof Button> give you?

Primary source

The official guidance is React's TypeScript page, react.dev — Using TypeScript (see the children / ReactNode notes), and the typings themselves in @types/react. On ComponentProps, see Total TypeScript's ComponentProps helper. Best way to feel it: in the Playground, type a prop as JSX.Element, try to pass a string, then widen it to ReactNode.

Try it yourself

Hit a “Type 'string' is not assignable to ReactElement” error on a children prop? Ask whether the slot is a wide render slot or a single element, and widen to ReactNode if it's the former. Next lesson (4.5) reads another inference-heavy library type — Zustand's curried create<State>()(...) and its StateCreator.