Chapter 1 of 4

Typing Props

The base patterns for component props, including children.

A component's props are just an object, so you type them like any other object. Most codebases declare an interface or a type alias and annotate the parameter.

interface ButtonProps {
  label: string;
  variant?: "primary" | "ghost";     // optional, with a union of allowed values
  onClick: () => void;
}

function Button({ label, variant = "primary", onClick }: ButtonProps) {
  return <button className={variant} onClick={onClick}>{label}</button>;
}

interface or type?

For props either works. interface can be extended and merged; type can express unions and intersections. Pick one for consistency - many teams use interface for props and type for everything else.

Typing children

ReactNode covers everything React can render: elements, strings, numbers, arrays, null and undefined.

interface CardProps {
  title: string;
  children: React.ReactNode;
}

// Optional children
interface PanelProps {
  children?: React.ReactNode;
}
// Avoid
const Button: React.FC<ButtonProps> = ({ label }) => <button>{label}</button>;

// Prefer
function Button({ label }: ButtonProps) {
  return <button>{label}</button>;
}

Extending an HTML element's props

A wrapper component should usually accept everything the underlying element does. ComponentProps pulls those types in for you.

interface ButtonProps extends React.ComponentProps<"button"> {
  variant?: "primary" | "ghost";
}

function Button({ variant = "primary", ...rest }: ButtonProps) {
  return <button className={variant} {...rest} />;
}

// onClick, disabled, type, aria-label - all typed correctly

Unions for mutually exclusive props

type AlertProps =
  | { severity: "error"; retry: () => void }
  | { severity: "info"; retry?: never };

// TypeScript now requires retry for errors and forbids it for info