Skip to content
React rc typescript 4 min read

Typing Props & Children

Typing props is where TypeScript earns its keep in a React codebase. A well-typed props contract documents exactly what a component accepts, catches mistakes at the call site instead of at runtime, and powers editor autocomplete for everyone who uses the component. This page covers how to describe props with interfaces and type aliases, handle optional and default values, type the children prop correctly, and model mutually exclusive props with discriminated unions.

Interfaces vs type aliases

You can describe a component’s props with either an interface or a type alias. Both work, and for plain object shapes they behave almost identically. Pass the type as a generic-style annotation on the destructured props parameter.

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 props type after the component with a Props suffix. Use interface for public, extendable component contracts (it supports declaration merging and extends), and use a type alias when you need unions, intersections, or mapped types — things interfaces cannot express.

Annotate the props parameter directly ({ label }: ButtonProps) rather than typing the function with React.FC. React.FC adds an implicit children prop, complicates generics, and the React team no longer recommends it.

Optional props and defaults

Mark a prop optional with ?. An optional prop has the type T | undefined, so TypeScript 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} />;
}

Here alt and size may be omitted by the caller, and the defaults fill them in. Because the default makes the value non-undefined inside the function body, you can use size directly in arithmetic without a guard.

PatternMeaning at the call siteInside the component
name: stringRequired — must be passedAlways a string
name?: stringOptional — may be omittedstring | undefined
name = "Guest" (default)OptionalAlways a string

Typing children

The children prop is just another prop, and React provides ReactNode to type it. ReactNode accepts everything React can render: elements, strings, numbers, fragments, arrays, null, and undefined.

import { 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 React element rather than arbitrary content, use ReactElement instead. To accept a render function as children, type it as a function returning ReactNode.

import { ReactNode } from "react";

interface ToggleProps {
  children: (isOn: boolean) => ReactNode;
}

function Toggle({ children }: ToggleProps) {
  const isOn = true;
  return <>{children(isOn)}</>;
}

Union and discriminated props

Sometimes a component supports several mutually exclusive configurations. A plain union of strings handles simple cases, but a discriminated union lets you make 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") {
    // `retry` is only available in this branch
    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 if check, TypeScript narrows props to the matching variant, so props.retry is available only for the error case. An info alert that tried to pass retry, or an error alert that omitted it, would be 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 (onClick, aria-*, type, and so on) for free. ComponentProps reads the prop type of any element or component.

import { 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. Spreading ...rest forwards those attributes to the underlying element.

Best Practices

  • Annotate the destructured props parameter with an interface or type; avoid React.FC.
  • Suffix prop types with Props and keep them next to the component that uses them.
  • Mark genuinely optional props with ? and supply defaults in the destructuring pattern.
  • Type children as ReactNode, or ReactElement/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 precise literal unions ("primary" | "secondary") over a loose string for finite options.
Last updated June 14, 2026
Was this helpful?