Typing Props

Difficulty: Advanced

Typing component props is the most fundamental skill when using TypeScript with React. Props define the contract between a parent component and its child, and TypeScript enforces that contract at compile time. The most common approach is to define a props interface and annotate the component parameter, giving you autocomplete, refactoring support, and compile-time error detection.

There are two main ways to type a React function component. The first is the explicit `React.FC<Props>` (or `React.FunctionComponent<Props>`) generic type, which automatically includes the `children` prop in older React types. The second and now more recommended approach is to simply type the props parameter directly: `function MyComponent(props: MyProps)`. The direct typing approach is preferred in modern React because it gives you more control over whether `children` is accepted and avoids some subtle issues with FC's implicit children and return type.

The `children` prop is typed as `React.ReactNode`, which covers strings, numbers, JSX elements, arrays, fragments, portals, null, and undefined - essentially anything React can render. When using the direct typing approach, you must explicitly add `children` to your props interface. You can also use the utility type `React.PropsWithChildren<Props>` to add a typed `children` prop to any interface.

Optional props are defined with the `?` modifier, and default values can be provided via destructuring defaults in the function parameter. Required props without `?` will cause a compile error if omitted by the parent. This makes it impossible to forget a required prop, catching bugs at development time rather than in production.

Code examples

Basic Props Interface

// Defining and using typed props
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary' | 'danger';
  disabled?: boolean;
}

// Direct typing approach (recommended)
function Button({ label, onClick, variant = 'primary', disabled = false }: ButtonProps) {
  return `<button class="${variant}" disabled={${disabled}}>${label}</button>`;
}

// Simulating component usage
const result1 = Button({ label: 'Submit', onClick: () => {}, variant: 'primary' });
const result2 = Button({ label: 'Cancel', onClick: () => {}, variant: 'danger', disabled: true });
const result3 = Button({ label: 'Save', onClick: () => {} }); // uses defaults

console.log(result1);
console.log(result2);
console.log(result3);

Props with '?' are optional. Default values are provided via destructuring. TypeScript ensures the parent passes all required props and that values match the expected types.

Children Prop and PropsWithChildren

// Typing the children prop explicitly
interface CardProps {
  title: string;
  children: string; // In real React, this would be React.ReactNode
}

function Card({ title, children }: CardProps): string {
  return `<div class="card"><h2>${title}</h2><div>${children}</div></div>`;
}

// PropsWithChildren utility type pattern
// type PropsWithChildren<P> = P & { children?: React.ReactNode };

interface PanelProps {
  heading: string;
}

// Simulating PropsWithChildren
type WithChildren<P> = P & { children?: string };

function Panel({ heading, children }: WithChildren<PanelProps>): string {
  return `<section><h3>${heading}</h3>${children || 'No content'}</section>`;
}

console.log(Card({ title: 'Welcome', children: 'Hello there!' }));
console.log(Panel({ heading: 'Info', children: 'Details here' }));
console.log(Panel({ heading: 'Empty' }));

Children can be typed as React.ReactNode (accepting anything renderable) or more narrowly. PropsWithChildren<P> adds an optional children prop to any props type.

Discriminated Union Props

// Using discriminated unions for polymorphic component props
type AlertProps =
  | { type: 'success'; message: string }
  | { type: 'error'; message: string; retryable: boolean }
  | { type: 'loading' };

function Alert(props: AlertProps): string {
  switch (props.type) {
    case 'success':
      return `[SUCCESS] ${props.message}`;
    case 'error':
      return `[ERROR] ${props.message} (retryable: ${props.retryable})`;
    case 'loading':
      return '[LOADING] Please wait...';
  }
}

console.log(Alert({ type: 'success', message: 'Saved!' }));
console.log(Alert({ type: 'error', message: 'Failed', retryable: true }));
console.log(Alert({ type: 'loading' }));

Discriminated union props let you model components where different prop combinations are valid depending on a discriminant field. TypeScript narrows the type inside each case.

Key points

Concepts covered

FC type, props interfaces, children prop, optional props, default props, React.PropsWithChildren