Typing React Props
Type component props with required/optional fields, children, unions for variants, and HTML attribute extension patterns.
- typescript
- typing-react
Props are the public API of a component. Good prop types catch misuse at the call site: wrong variant strings, missing handlers, invalid children. Bad prop types (any, loose index signatures) make React TypeScript pointless.
Docs: React TypeScript Cheatsheet, React components.
Basic props type
type UserCardProps = {
name: string;
bio?: string;
onFollow?: () => void;
};
function UserCard({ name, bio, onFollow }: UserCardProps) {
return (
<article>
<h2>{name}</h2>
{bio ? <p>{bio}</p> : null}
{onFollow ? (
<button type="button" onClick={onFollow}>
Follow
</button>
) : null}
</article>
);
}
Optional props use ?. Don’t make everything optional “for flexibility.”
children
type PanelProps = {
title: string;
children: React.ReactNode;
};
function Panel({ title, children }: PanelProps) {
return (
<section>
<h2>{title}</h2>
{children}
</section>
);
}
ReactNode covers elements, strings, numbers, fragments, portals, arrays, null. Use ReactElement only when you require an actual element.
Variant unions
type ButtonProps = {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
type?: 'button' | 'submit' | 'reset';
};
function Button({
variant = 'primary',
size = 'md',
type = 'button',
children,
...rest
}: ButtonProps) {
return (
<button type={type} data-variant={variant} data-size={size} {...rest}>
{children}
</button>
);
}
See literal types.
Extending native element props
type TextFieldProps = {
label: string;
error?: string;
} & React.ComponentPropsWithoutRef<'input'>;
function TextField({ label, error, id, ...inputProps }: TextFieldProps) {
const fieldId = id ?? inputProps.name;
return (
<div>
<label htmlFor={fieldId}>{label}</label>
<input id={fieldId} aria-invalid={Boolean(error)} {...inputProps} />
{error ? <p role="alert">{error}</p> : null}
</div>
);
}
ComponentPropsWithoutRef<'input'> pulls native attributes; WithRef when you forward refs.
Discriminated props
type LinkButtonProps =
| { as: 'a'; href: string; onClick?: never }
| { as: 'button'; href?: never; onClick: () => void };
function LinkButton(props: LinkButtonProps & { children: React.ReactNode }) {
if (props.as === 'a') {
return <a href={props.href}>{props.children}</a>;
}
return (
<button type="button" onClick={props.onClick}>
{props.children}
</button>
);
}
FC or not?
// Older style
const Badge: React.FC<{ label: string }> = ({ label }) => <span>{label}</span>;
Modern preference: annotate props on a function declaration; avoid FC unless you want its implicit children behavior (which changed across versions). Explicit children is clearer.
Default props
Prefer default values in destructuring over Component.defaultProps (legacy for functions).
Footguns
props: any— disable value of TS.- Re-defining
className/stylepoorly when extending HTML. React.FC+ generics — awkward; use function declarations.- Optional callback vs required — if the button is useless without
onClick, require it.
Interview out-loud answer
“I type props as an object type or interface, use unions for variants, ReactNode for children, and ComponentPropsWithoutRef when wrapping native elements. Discriminated unions model mutually exclusive prop sets. I avoid any and prefer explicit children over relying on FC.”
Polymorphic as prop (sketch)
type BoxProps<T extends React.ElementType = 'div'> = {
as?: T;
children?: React.ReactNode;
} & Omit<React.ComponentPropsWithoutRef<T>, 'as' | 'children'>;
Polymorphic components are powerful and easy to mistype — add them when the design system truly needs them, not for one-off cases.
Related on this site
- Typing React events
- Generics in React components
- Literal types
- Discriminated unions for UI state
- Interfaces vs type aliases
Further reading
Related guides
- Typing React EventsCorrect event types for onClick, onChange, forms, and keyboard handlers in React TypeScript — synthetic events and target narrowing.
- Basic Types and AnnotationsPrimitives, arrays, objects, and function annotations TypeScript actually checks — plus where inference is enough and where it is not.
- Branded Types for IDsNominal-style UserId vs OrderId in TypeScript — prevent ID mixups at compile time with brands, parsers, and form boundaries.
- Conditional Types IntroT extends U ? X : Y — how TypeScript picks types from conditions, distributes over unions, and powers utility types you already use.
- Declaration Files and DefinitelyTypedHow .d.ts files describe JS to TypeScript, when to use @types packages, module augmentation, and writing minimal ambient types for untyped libs.