Typing Props
Props are the public contract of a React component, and TypeScript turns that contract into something the compiler enforces. A well-typed props type documents exactly what a component accepts, catches mistakes at the call site, and powers editor autocomplete for everyone who uses it. This page covers describing props with interfaces and type aliases, handling optional values and defaults, typing the children prop, modeling mutually exclusive props with discriminated unions, and why you should avoid React.FC.
Interfaces vs type aliases
Describe a component’s props with either an interface or a type alias and annotate the destructured parameter directly. For plain object shapes the two behave almost identically.
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 type after the component with a Props suffix. Reach for interface on public, extendable contracts (it supports extends and declaration merging), and a type alias when you need unions, intersections, or mapped types that interfaces cannot express.
Annotate the props parameter directly (
{ label }: ButtonProps) rather than typing the function withReact.FC.React.FCadds an implicitchildren, complicates generic components, and the React team no longer recommends it.
Optional props and defaults
Mark a prop optional with ?; its type becomes T | undefined, so the compiler 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} />;
}
Because the default removes undefined inside the body, you can use size directly in arithmetic without a guard.
| Pattern | At the call site | Inside the component |
|---|---|---|
name: string | Required | Always string |
name?: string | Optional | string | undefined |
name = "Guest" | Optional | Always string |
Typing children
The children prop is just another prop, and React’s ReactNode types it. ReactNode accepts everything React can render: elements, strings, numbers, fragments, arrays, null, and undefined.
import type { 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 element, use ReactElement instead. To accept a render function, type children as a function returning ReactNode.
Union and discriminated props
When a component supports mutually exclusive configurations, a discriminated union lets 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") {
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 check, TypeScript narrows props to the matching variant, so props.retry exists only in the error branch — passing retry to an info alert is 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 for free. ComponentProps reads the prop type of any element or component.
import type { 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, and 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 their component. - 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 literal unions (
"primary" | "secondary") over a loosestringfor finite options.