Typing Props & Children
Typing props is where TypeScript earns its keep in a React codebase. A well-typed props contract documents exactly what a component accepts, catches mistakes at the call site instead of at runtime, and powers editor autocomplete for everyone who uses the component. This page covers how to describe props with interfaces and type aliases, handle optional and default values, type the children prop correctly, and model mutually exclusive props with discriminated unions.
Interfaces vs type aliases
You can describe a component’s props with either an interface or a type alias. Both work, and for plain object shapes they behave almost identically. Pass the type as a generic-style annotation on the destructured props parameter.
interface ButtonProps {
label: string;
variant: "primary" | "secondary";
disabled: boolean;
}
function Button({ label, variant, disabled }: ButtonProps) {
return (
<button className={variant} disabled={disabled}>
{label}
</button>
);
}
The convention is to name the props type after the component with a Props suffix. Use interface for public, extendable component contracts (it supports declaration merging and extends), and use a type alias when you need unions, intersections, or mapped types — things interfaces cannot express.
Annotate the props parameter directly (
{ label }: ButtonProps) rather than typing the function withReact.FC.React.FCadds an implicitchildrenprop, complicates generics, and the React team no longer recommends it.
Optional props and defaults
Mark a prop optional with ?. An optional prop has the type T | undefined, so TypeScript forces you to handle the missing case. The cleanest way to supply a fallback is a default value in the destructuring pattern.
interface AvatarProps {
src: string;
alt?: string;
size?: number;
}
function Avatar({ src, alt = "User avatar", size = 40 }: AvatarProps) {
return <img src={src} alt={alt} width={size} height={size} />;
}
Here alt and size may be omitted by the caller, and the defaults fill them in. Because the default makes the value non-undefined inside the function body, you can use size directly in arithmetic without a guard.
| Pattern | Meaning at the call site | Inside the component |
|---|---|---|
name: string | Required — must be passed | Always a string |
name?: string | Optional — may be omitted | string | undefined |
name = "Guest" (default) | Optional | Always a string |
Typing children
The children prop is just another prop, and React provides ReactNode to type it. ReactNode accepts everything React can render: elements, strings, numbers, fragments, arrays, null, and undefined.
import { ReactNode } from "react";
interface CardProps {
title: string;
children: ReactNode;
}
function Card({ title, children }: CardProps) {
return (
<section className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</section>
);
}
If a component requires a single React element rather than arbitrary content, use ReactElement instead. To accept a render function as children, type it as a function returning ReactNode.
import { ReactNode } from "react";
interface ToggleProps {
children: (isOn: boolean) => ReactNode;
}
function Toggle({ children }: ToggleProps) {
const isOn = true;
return <>{children(isOn)}</>;
}
Union and discriminated props
Sometimes a component supports several mutually exclusive configurations. A plain union of strings handles simple cases, but a discriminated union lets you make whole groups of props depend on one another — so the type system rejects invalid combinations.
type AlertProps =
| { kind: "error"; message: string; retry: () => void }
| { kind: "info"; message: string };
function Alert(props: AlertProps) {
if (props.kind === "error") {
// `retry` is only available in this branch
return (
<div role="alert">
{props.message} <button onClick={props.retry}>Retry</button>
</div>
);
}
return <div>{props.message}</div>;
}
The kind field is the discriminant. After the if check, TypeScript narrows props to the matching variant, so props.retry is available only for the error case. An info alert that tried to pass retry, or an error alert that omitted it, would be a compile error at the call site.
Reusing native element props
To build wrappers around HTML elements, extend the built-in prop types so callers get every standard attribute (onClick, aria-*, type, and so on) for free. ComponentProps reads the prop type of any element or component.
import { ComponentProps } from "react";
interface IconButtonProps extends ComponentProps<"button"> {
icon: string;
}
function IconButton({ icon, ...rest }: IconButtonProps) {
return (
<button {...rest}>
<span className="icon">{icon}</span>
</button>
);
}
IconButton now accepts icon plus the entire native <button> API. Spreading ...rest forwards those attributes to the underlying element.
Best Practices
- Annotate the destructured props parameter with an
interfaceortype; avoidReact.FC. - Suffix prop types with
Propsand keep them next to the component that uses them. - Mark genuinely optional props with
?and supply defaults in the destructuring pattern. - Type
childrenasReactNode, orReactElement/a function when you need something stricter. - Reach for discriminated unions to forbid invalid prop combinations at compile time.
- Extend
ComponentProps<"element">to inherit native attributes instead of redeclaring them. - Prefer precise literal unions (
"primary" | "secondary") over a loosestringfor finite options.