Lesson 27 · Module 4.4 — Library-grade patterns
ComponentProps cheatThree 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.
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.
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.
| you see… | read it as | accepts |
|---|---|---|
| children: ReactNode | a render slot | anything renderable |
| (): ReactElement | returns one element | a single element only |
| icon: JSX.Element | one element prop | a 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 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.
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.
Recall, don't re-read. (Answers reveal on click.)
children prop usually have, and why?ReactElement and JSX.Element relate?ComponentProps<typeof Button> give you?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.
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.