React + TypeScript: The Complete Typing Surface

Learn the full vocabulary TypeScript gives you for typing React code — component props, children, JSX return types, hooks, refs, context, reducers, and the utility types React itself ships — so you can type any component correctly without guessing or reaching for any.

Every React app written in TypeScript eventually needs to type the same handful of things over and over: what a component accepts as props, what it's allowed to return, what a ref points to, what a context value looks like, and what shape a hook's return value has. React and the DOM typings ship a large set of purpose-built types for exactly these situations — ReactNode, ReactElement, ComponentProps, PropsWithChildren, ForwardedRef, and more. Knowing which one applies to which situation is the difference between fighting the type checker and writing code where TypeScript simply confirms what you already know is true.

Typing props and children

The starting point for almost every component is a props type — an object type or interface describing what the component accepts, written above the component and passed as the parameter's type annotation: function Card({ title, children }: CardProps). There's no special 'React props type' beyond a normal TypeScript object type; the only React-specific piece is what type you give to children.

children should almost always be typed as React.ReactNode, not JSX.Element or React.ReactElement. ReactNode is the broad, permissive type — it covers a single element, an array of elements, a string, a number, null, undefined, and booleans (which render as nothing), matching everything React is actually willing to render. ReactElement, by contrast, is narrower: it describes the specific object shape returned by <Foo /> or React.createElement, and using it for children incorrectly rejects perfectly valid children like plain strings or fragments.

Rather than writing children: React.ReactNode by hand in every props type, React ships a helper: React.PropsWithChildren<Props> takes your existing props type and adds a children: ReactNode field to it, saving the repetition and keeping the convention consistent across a codebase.

What a component is allowed to return

A function component's return type is inferred automatically in almost all cases, so you rarely annotate it explicitly — but it's worth knowing what that inferred type actually is. In modern React types, a function component returns React.ReactNode, which is why returning null, a string, an array of elements, or a full JSX tree from the same component are all valid without any special-casing.

React.ReactElement shows up in a different place: as the return type of React.createElement itself, and as a narrower type you reach for when you specifically need 'exactly one JSX element, not a string or null or an array' — for example, typing a render prop that must produce a single element, or typing the argument to React.cloneElement, which requires an actual element object to clone.

JSX.Element is an older, closely related type that many codebases still use interchangeably with ReactElement for a component's return type; it's effectively ReactElement<any, any> under the hood. New code generally prefers letting the return type infer, or using ReactNode/ReactElement explicitly depending on how permissive the signature needs to be.

Hooks, refs, context, and reducers — a quick map

Most built-in hooks infer their types from how you call them, so useState(0) already knows the state is number without any annotation — you only add an explicit type argument, like useState<User | null>(null), when the initial value doesn't fully describe the type you'll eventually store (a nullable value being the most common case). Event handler parameters use React's synthetic event types, such as React.ChangeEvent<HTMLInputElement> or React.MouseEvent<HTMLButtonElement> — this is covered in full depth in the dedicated Events lesson, so here it's enough to know the pattern exists and follows the same <HTMLElementType> shape everywhere.

A ref created with useRef<HTMLDivElement>(null) is typed by its type argument, giving you RefObject<HTMLDivElement> whose .current starts as null and is later attached by React itself; this is covered in full in the Refs lesson. Context follows the same idea: createContext<ContextValueType | undefined>(undefined) gives every consumer of that context the exact value shape, with undefined in the union representing 'no provider is present yet,' which you narrow away inside a custom hook that throws if the value is missing.

useReducer<React.Reducer<State, Action>> (or simply letting it infer from a reducer function that's already fully typed) ties state and dispatch together so that dispatch only accepts Action objects matching your defined union — full coverage of this pattern lives in the useReducer Patterns lesson. The common thread across all four of these hooks is the same: define the shape as a named type first, then hand it to the hook as a type argument or let it flow in from an already-typed value.

Generic components and generic hooks, briefly

A component can itself be generic over the data it works with, the same way a function can be generic — function List<T>({ items }: { items: T[] }) lets TypeScript infer T from whatever array is passed in at each call site, giving you full type safety without writing a separate component per data shape. This is covered in full depth, with worked examples like a generic table and generic form, in the Generic React Components module — the summary here is only that the capability exists and follows ordinary TypeScript generic syntax.

Custom hooks can be generic in exactly the same way: a hook like function useLocalStorage<T>(key: string, initial: T): [T, (value: T) => void] infers T from the initial value you pass in, so useLocalStorage('theme', 'dark') gives back a string-typed pair while useLocalStorage('user', null as User | null) gives back a User | null-typed pair from the same hook definition.

Utility types: ComponentProps and friends

React ships a family of utility types for extracting the props of an existing component or element rather than redeclaring them. React.ComponentProps<'button'> gives you every prop a native <button> accepts (including onClick, disabled, type, and all standard HTML attributes); React.ComponentProps<typeof MyComponent> does the same for one of your own components, reading its props type straight from its definition. This is invaluable when you're wrapping an existing component and want to accept 'whatever props it accepts' without copying its type definition by hand.

Two narrower variants exist for when refs are involved: ComponentPropsWithRef<T> includes the ref prop in the result (matching what you can actually pass to the underlying element or forwardRef component), while ComponentPropsWithoutRef<T> deliberately excludes it — the right choice when you're about to attach your own ref prop for forwarding and don't want a duplicate, conflicting ref entry in the extracted type.

ElementType types a value that can be used as a JSX tag — a string like 'div' or 'a', or a component reference like MyButton — and is the type behind the as prop pattern covered in the Polymorphic Components lesson. ComponentType<P> is the type of a component itself (function or class) that accepts props P, useful for props like icon: React.ComponentType<IconProps> where you're passed a component to render rather than an already-rendered element.

React.FC versus explicit props typing

Early React + TypeScript code commonly typed components with React.FC<Props>: const Card: React.FC<CardProps> = ({ title, children }) => .... This wraps the component's type, automatically adding an implicit, always-optional children: ReactNode prop and a return type of ReactElement | null, and gives you a named type for the component value itself.

The explicit-props alternative skips the wrapper entirely: function Card({ title, children }: CardProps) { ... }. This has become the generally preferred style for a few concrete reasons. React.FC used to type children as always present and optional on every component regardless of whether that component actually accepted children, which silently permitted <Card title="x"><SomeStray /></Card> on components never designed to render children. It also makes generic components awkward to write, since React.FC doesn't compose cleanly with a type parameter. Plain function declarations avoid both problems, infer the return type correctly on their own, and read as ordinary TypeScript rather than a React-specific wrapper.

React.FC isn't 'wrong,' and you'll still see it in production codebases and some style guides — but for new code, explicit props typing on a plain function (or arrow function) is the more broadly recommended default, and it's the style used throughout the rest of this course.

forwardRef typing and native HTML attribute types

React.forwardRef is generic over two type parameters: the type of the thing the ref will point to, and the props type — written as forwardRef<HTMLInputElement, InputProps>((props, ref) => ...). Inside the callback, ref is typed as React.ForwardedRef<HTMLInputElement>, a type that's a bit more permissive than RefObject because a forwarded ref can also be a callback function, not just an object with .current. Full detail on the forwarding mechanics lives in the Refs: Forwarding and Imperative Handles lesson; the typing piece is simply matching the two type arguments to what the component actually holds and accepts.

For plain HTML attributes, the DOM typings define one interface per element category — React.ButtonHTMLAttributes<HTMLButtonElement>, React.InputHTMLAttributes<HTMLInputElement>, React.HTMLAttributes<HTMLDivElement>, and so on — which is exactly the subject of the next lesson, Native HTML Prop Typing. Briefly here: these interfaces are what let a component spread ...rest props onto a native element and have TypeScript verify every one of them is a real, valid attribute for that element.

CSS values passed through the style prop are typed with React.CSSProperties, an interface covering every standard CSS property (camelCased, like backgroundColor and fontSize) with the correct value types — numbers where CSS accepts unitless numbers, strings elsewhere. This keeps a typo like colr: 'red' or an invalid value from silently doing nothing at runtime; the type checker catches it before the component ever renders.

The typing vocabulary at a glance

ReactNode is the permissive 'anything renderable' type — reach for it first for children and return types. ReactElement is narrower — 'exactly one JSX element object' — use it for render props and cloneElement arguments. ComponentProps variants let you reuse an existing component's or element's prop types instead of redeclaring them.

// Props with children
type CardProps = React.PropsWithChildren<{ title: string }>;

// Explicit props (preferred over React.FC)
function Card({ title, children }: CardProps) {
  return (
    <div>
      <h2>{title}</h2>
      {children}
    </div>
  );
}

// Extracting another component's or element's props
type NativeButtonProps = React.ComponentPropsWithoutRef<'button'>;
type WrappedProps = React.ComponentProps<typeof Card>;

// forwardRef with two type arguments: <RefType, PropsType>
const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
  (props, ref) => <input ref={ref} {...props} />
);

// A component that accepts a component reference, not a rendered element
type IconSlotProps = { icon: React.ComponentType<{ size?: number }> };

Code Examples

PropsWithChildren adds a correctly typed children: ReactNode field onto the plain object type, so the component accepts both a specific kind prop and any renderable children without writing the children field by hand. Expected result: Renders a styled div; TypeScript rejects kind="danger" since it isn't in the union.

ComponentPropsWithoutRef<'button'> pulls in every valid native button attribute (onClick, type, form, aria-* attributes, and so on) so LoadingButton behaves like a real button plus isLoading. ComponentType<{ size?: number }> types a prop that holds a component itself, which is then rendered as <Icon size={16} /> inside the function.

type LoadingButtonProps = React.ComponentPropsWithoutRef<'button'> & { isLoading?: boolean };

type AlertProps = React.PropsWithChildren<{
  kind: 'info' | 'warning' | 'error';
}>;

function Alert({ kind, children }: AlertProps) {
  return <div className={`alert alert-${kind}`}>{children}</div>;
}

// Usage
<Alert kind="warning">Disk space is low.</Alert>;
// A wrapper that should accept anything a real <button> accepts,
// plus one extra prop of its own.
type LoadingButtonProps = React.ComponentPropsWithoutRef<'button'> & {
  isLoading?: boolean;
};

function LoadingButton({ isLoading, children, disabled, ...rest }: LoadingButtonProps) {
  return (
    <button disabled={disabled || isLoading} {...rest}>
      {isLoading ? 'Loading…' : children}
    </button>
  );
}

// icon: a component reference, not JSX
function ToolbarButton({ icon: Icon }: { icon: React.ComponentType<{ size?: number }> }) {
  return (
    <button>
      <Icon size={16} />
    </button>
  );
}

Which type for which job

A quick lookup table for the type that matches each common situation.

Situation                          Type to reach for
  ----------------------------------  --------------------------
  children prop                       React.ReactNode
  "exactly one element" (render prop) React.ReactElement
  a component's own return value      inferred (ReactNode)
  reuse another component's props     ComponentProps<typeof X>
  ...same, but you add your own ref   ComponentPropsWithoutRef<X>
  a prop that IS a component          ComponentType<Props>
  a prop that can be a tag or comp.   ElementType
  ref inside forwardRef               ForwardedRef<T>
  style prop values                   CSSProperties

Important Rules

Type children (and permissive return values) as React.ReactNode, not ReactElement or JSX.Element, unless you specifically need to require exactly one element.

Prefer explicit props typing on a plain function over React.FC for new components — it avoids the implicit-children issue and composes better with generics.

Reach for ComponentProps / ComponentPropsWithoutRef to reuse an existing element's or component's prop types instead of hand-copying HTML attributes.

Give useState, useRef, and createContext an explicit type argument whenever the initial value doesn't fully describe the eventual type (most commonly, a nullable value).

Where this matters

Design-system and component-library code leans on this whole vocabulary constantly: a Button reuses ComponentPropsWithoutRef<'button'> so consumers get full native behavior for free, an Icon prop is typed with ComponentType so any icon component can be swapped in, and a shared Modal's children is typed as ReactNode so it can host anything from plain text to a full form. Getting these types right once, at the base of a component library, is what lets every consuming team get accurate autocomplete and compile-time errors instead of runtime surprises.

type Props = { children: React.ReactElement };

type Props = { children: React.ReactNode };

Typing children as ReactElement rejects valid children like plain strings, numbers, fragments, and arrays of elements, because ReactElement describes exactly one element object, not the full range of things React can render.

const Card: React.FC<CardProps> = ({ title }) => <h2>{title}</h2>;

function Card({ title }: CardProps) { return <h2>{title}</h2>; }

React.FC silently adds an optional children prop to every component regardless of whether it should accept children, and it composes poorly with generic components — explicit props typing on a plain function avoids both issues.

[multiple-choice] Why is React.ReactNode usually the right type for a children prop, rather than React.ReactElement? A. ReactNode is shorter to type B. ReactElement only describes a single element object, while ReactNode also covers strings, numbers, arrays, and null/undefined C. ReactElement doesn't exist in modern React types D. ReactNode is required by JSX syntax

B. ReactElement only describes a single element object, while ReactNode also covers strings, numbers, arrays, and null/undefined — ReactElement is narrow — exactly one element object — while ReactNode covers everything React can actually render, which is what children realistically needs to accept.

[multiple-choice] What is the main reason explicit props typing is now generally preferred over React.FC? A. React.FC is deprecated and produces a compiler error B. React.FC silently adds an always-optional children prop to every component and composes poorly with generics C. React.FC cannot be used with TypeScript at all D. Explicit props typing is faster at runtime

B. React.FC silently adds an always-optional children prop to every component and composes poorly with generics — React.FC's implicit children prop lets children be passed to components that were never designed to accept them, and it doesn't compose cleanly with generic type parameters — explicit function typing avoids both problems.

[multiple-choice] Which utility type extracts a native element's or existing component's props, excluding the ref prop, so you can attach your own via forwardRef? A. ComponentType B. PropsWithChildren C. ComponentPropsWithoutRef D. ElementType

C. ComponentPropsWithoutRef — ComponentPropsWithoutRef<T> pulls in the target's props but omits ref, which is exactly what you want when you're about to add your own ref via forwardRef and don't want a conflicting duplicate.

Type a Tooltip component that accepts all native div attributes, a required label: string prop for the tooltip text, and renders its children inside the div alongside the label. Use ComponentPropsWithoutRef to pull in the native attributes.

type TooltipProps = React.ComponentPropsWithoutRef<'div'> & { label: string; }; function Tooltip({ label, children, ...rest }: TooltipProps) { return ( <div {...rest}> {children} <span className="tooltip-label">{label}</span> </div> ); }

Intersect ComponentPropsWithoutRef<'div'> with an object type containing label: string, and destructure label and children out before spreading ...rest onto the div.