Typing Props
Recipe
Define clear, type-safe prop contracts for your React components using TypeScript interfaces and type aliases. Cover required props, optional props, children, and common prop patterns.
Working Example
// Basic props with required and optional fields
type ButtonProps = {
label: string;
variant?: "primary" | "secondary" | "danger";
disabled?: boolean;
onClick: () => void;
};
export function Button({ label, variant = "primary", disabled = false, onClick }: ButtonProps) {
return (
<button className={`btn btn-${variant}`} disabled={disabled} onClick={onClick}>
{label}
</button>
);
}// Typing children
type CardProps = {
title: string;
children: React.ReactNode;
};
export function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}// Render prop pattern
type DataListProps<T> = {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
};
export function DataList<T>({ items, renderItem }: DataListProps<T>) {
return <ul>{items.map((item, i) => <li key={i}>{renderItem(item, i)}</li>)}</ul>;
}// Usage
<Button label="Submit" onClick={() => console.log("clicked")} />
<Card title="Welcome"><p>Hello world</p></Card>
<DataList items={["a", "b"]} renderItem={(item) => <span>{item}</span>} />Deep Dive
How It Works
- Props types act as a contract between the component and its consumers. TypeScript enforces that every required prop is supplied and every value matches the expected type.
React.ReactNodeis the broadest children type. It accepts strings, numbers, JSX elements, arrays, fragments,null, andundefined.React.ReactElementis narrower thanReactNode. Use it when you specifically need a JSX element (not a string or number).- Default values in destructuring (e.g.,
variant = "primary") work seamlessly with optional props. TypeScript infers the narrowed type inside the function body. - Union literal types like
"primary" | "secondary"give you autocomplete and catch typos at compile time.
Variations
Extending HTML element props:
type InputProps = React.ComponentPropsWithoutRef<"input"> & {
label: string;
error?: string;
};
export function Input({ label, error, ...rest }: InputProps) {
return (
<div>
<label>{label}</label>
<input {...rest} />
{error && <span className="error">{error}</span>}
</div>
);
}Props with component injection:
type LayoutProps = {
as?: React.ElementType;
children: React.ReactNode;
className?: string;
};
export function Layout({ as: Component = "div", children, className }: LayoutProps) {
return <Component className={className}>{children}</Component>;
}TypeScript Notes
- Use
typefor props that use unions or intersections. Useinterfacewhen you needextendsor want declaration merging. React.PropsWithChildren<T>is a shorthand that addschildren?: React.ReactNodeto your type.React.ComponentPropsWithRef<"div">includes therefprop;ComponentPropsWithoutRef<"div">excludes it.
Gotchas
- Using
React.FCadds an implicitchildrenprop in older React types (pre-18). In React 18+ types,React.FCno longer includeschildrenimplicitly. - Typing children as
JSX.Elementwill reject strings, numbers, and arrays. UseReact.ReactNodeunless you have a specific reason. - Spreading
...restonto a DOM element without filtering custom props causes React warnings about unknown DOM attributes. - Forgetting to export your props type makes it impossible for consumers to reuse or extend it.
Alternatives
| Approach | Pros | Cons |
|---|---|---|
type alias | Supports unions, intersections, mapped types | No declaration merging |
interface | Extendable, familiar OOP pattern | Cannot express union types directly |
React.FC<Props> | Explicit return type annotation | Verbose, no generic component support |
| Inline prop types | Quick for throwaway components | Hard to reuse or export |
PropsWithChildren | Convenient children shorthand | Hides the children prop from readers |
FAQs
Should you use type or interface for component props?
- Use
typewhen you need unions, intersections, or mapped types. - Use
interfacewhen you needextendsor declaration merging. - For most React components,
typeis sufficient and more flexible.
What is the difference between React.ReactNode and React.ReactElement for children?
ReactNodeaccepts strings, numbers, JSX elements, arrays, fragments,null, andundefined.ReactElementonly accepts JSX elements (not strings or numbers).- Use
ReactNodeunless you specifically need to restrict children to JSX elements.
How do you extend native HTML element props for a custom component?
type InputProps = React.ComponentPropsWithoutRef<"input"> & {
label: string;
error?: string;
};
function Input({ label, error, ...rest }: InputProps) {
return (
<div>
<label>{label}</label>
<input {...rest} />
{error && <span>{error}</span>}
</div>
);
}- Use
ComponentPropsWithoutRef<"element">to get all native props. - Intersect with your custom props using
&.
Gotcha: What happens if you spread ...rest onto a DOM element without filtering custom props?
- React will warn about unknown DOM attributes.
- Custom props like
labelorerrorget passed to the HTML element. - Destructure custom props out before spreading the rest.
How do default values work with optional props in TypeScript?
type ButtonProps = {
variant?: "primary" | "secondary";
};
function Button({ variant = "primary" }: ButtonProps) {
// TypeScript narrows variant to "primary" | "secondary" (not undefined)
}- Mark the prop as optional with
?in the type. - Provide the default in the destructuring pattern.
- TypeScript infers the narrowed type inside the function body.
What is React.PropsWithChildren<T> and when should you use it?
- A shorthand that adds
children?: React.ReactNodeto your type. - Convenient but hides the
childrenprop from readers of the type definition. - Prefer explicit
children: React.ReactNodein the type for clarity.
Gotcha: Does React.FC include children in its type?
- In React 18+ types,
React.FCno longer includeschildrenimplicitly. - In older React types (pre-18), it added an implicit
childrenprop. - If you need children, declare it explicitly in your props type.
How do you type a render prop?
type DataListProps<T> = {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
};- The render prop is a function that returns
React.ReactNode. - Use a generic to type the item parameter based on the data.
What is the as prop pattern for polymorphic components?
type LayoutProps = {
as?: React.ElementType;
children: React.ReactNode;
};
function Layout({ as: Component = "div", children }: LayoutProps) {
return <Component>{children}</Component>;
}React.ElementTypeaccepts string tags ("div","span") and component types.- The default is typically a semantic HTML element.
Why should you export your props type?
- Consumers may need to reuse or extend your component's props type.
- Without the export, they cannot build wrapper components or extract prop subsets.
- Always export props types for shared or library components.