SyntaxStudy
Sign Up
TypeScript Typing React Components and Props
TypeScript Beginner 1 min read

Typing React Components and Props

TypeScript and React are an excellent combination. React components are typed as functions returning JSX.Element or ReactNode. Props are described with an interface or type alias and passed as the type parameter to React.FC, or more commonly in modern React, as the parameter type directly in the function signature. The React.FC type adds children and some other implicit props, but the direct annotation approach is now generally preferred for clarity. Common prop patterns include optional props with defaults, union props for conditional rendering, and discriminated union props for component variants. Using required and optional props correctly prevents runtime errors from missing data and makes components self-documenting. Readonly props are the default for function components since you should never mutate props. Children props deserve special attention. ReactNode is the broadest type and accepts anything React can render — strings, numbers, elements, arrays, fragments, and portals. ReactElement is narrower and accepts only actual React elements. FC automatically includes children in older React types but requires you to declare it explicitly in newer @types/react versions. Being explicit about whether a component accepts children makes the API clearer.
Example
import React, { type ReactNode } from "react";

// Basic typed component — direct param annotation (preferred)
interface ButtonProps {
    label: string;
    variant?: "primary" | "secondary" | "danger";
    disabled?: boolean;
    onClick?: () => void;
    className?: string;
}

export function Button({
    label,
    variant = "primary",
    disabled = false,
    onClick,
    className = "",
}: ButtonProps): React.ReactElement {
    return (
        <button
            className={`btn btn-${variant} ${className}`}
            disabled={disabled}
            onClick={onClick}
        >
            {label}
        </button>
    );
}

// Discriminated union props — component variants
type AlertProps =
    | { type: "success"; message: string }
    | { type: "error";   message: string; retry?: () => void }
    | { type: "warning"; message: string; dismissible?: boolean };

export function Alert(props: AlertProps): React.ReactElement {
    const base = `alert alert-${props.type}`;
    if (props.type === "error") {
        return <div className={base}>{props.message} {props.retry && <button onClick={props.retry}>Retry</button>}</div>;
    }
    return <div className={base}>{props.message}</div>;
}

// Children prop
interface CardProps {
    title: string;
    children: ReactNode;
    footer?: ReactNode;
}

export function Card({ title, children, footer }: CardProps) {
    return (
        <div className="card">
            <h2>{title}</h2>
            <div className="card-body">{children}</div>
            {footer && <div className="card-footer">{footer}</div>}
        </div>
    );
}