Technology · TypeScript

TypeScript With React

Type React components, props, and hooks safely with TypeScript.

TL;DR
  1. 01Define prop interfaces or types for component APIs.
  2. 02Use React.FC<Props> or function return types for components.
  3. 03Extend built-in React types for events and refs.

Typing Component Props

  • Define prop types with an interface or type.
    interface ButtonProps {
      label: string;
      onClick: () => void;
      disabled?: boolean;
    }
    
    function Button({ label, onClick, disabled = false }: ButtonProps) {
      return <button onClick={onClick} disabled={disabled}>{label}</button>;
    }
  • Use React.FC for typed function components.
    interface CardProps {
      title: string;
      children: React.ReactNode;
    }
    
    const Card: React.FC<CardProps> = ({ title, children }) => (
      <div><h2>{title}</h2>{children}</div>
    );
  • Export prop types so consumers can extend them.
    export interface ButtonProps {
      label: string;
      onClick: () => void;
    }
    
    export function Button(props: ButtonProps) { }
  • Extend HTML element props for wrapper components.
    interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
      label: string;
    }
    
    function LabeledInput({ label, ...rest }: InputProps) {
      return (
        <label>
          {label}
          <input {...rest} />
        </label>
      );
    }
  • Use discriminated unions for multi-variant component props.
    type AlertProps =
      | { variant: "success"; message: string }
      | { variant: "error"; message: string; code: number };
    
    function Alert(props: AlertProps) {
      return <div className={props.variant}>{props.message}</div>;
    }

Event Handlers

  • Type event handlers with React event types.
    function Form() {
      const handleSubmit: React.FormEventHandler<HTMLFormElement> = (e) => {
        e.preventDefault();
        // Handle submission
      };
      
      return <form onSubmit={handleSubmit}></form>;
    }
  • Use specific event types for different inputs.
    const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
      console.log(e.target.value);
    };
    
    const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
      console.log(e.button);
    };
  • Annotate inline event handler parameters directly in JSX.
    <input onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
      setName(e.target.value);
    }} />
  • Type keyboard event handlers with React.KeyboardEvent.
    function SearchBox() {
      const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
        if (e.key === "Enter") submitSearch();
      };
      return <input onKeyDown={handleKeyDown} />;
    }
  • Pass typed event handler props for reusable components.
    interface TableRowProps {
      onRowClick: (id: number, e: React.MouseEvent<HTMLTableRowElement>) => void;
    }
    
    function TableRow({ onRowClick }: TableRowProps) {
      return <tr onClick={(e) => onRowClick(1, e)}></tr>;
    }

Typing Hooks

  • Type useState with a generic parameter.
    const [count, setCount] = useState<number>(0);
    const [user, setUser] = useState<User | null>(null);
  • Type useRef for DOM access.
    const inputRef = useRef<HTMLInputElement>(null);
    
    function focus() {
      inputRef.current?.focus();
    }
  • Type useContext with custom hooks.
    const UserContext = React.createContext<User | undefined>(undefined);
    
    function useUser() {
      const context = useContext(UserContext);
      if (!context) throw new Error("useUser needs provider");
      return context;
    }
  • Type useReducer with discriminated unions.
    type Action = 
      | { type: "INCREMENT"; payload: number }
      | { type: "DECREMENT"; payload: number };
    
    const reducer = (state: number, action: Action) => {
      switch (action.type) {
        case "INCREMENT": return state + action.payload;
        case "DECREMENT": return state - action.payload;
      }
    };
  • Annotate custom hook return types to improve caller inference.
    function useToggle(initial: boolean): [boolean, () => void] {
      const [value, setValue] = useState(initial);
      const toggle = () => setValue(v => !v);
      return [value, toggle];
    }

Generic Components

  • Build generic components for reusable logic.
    interface ListProps<T> {
      items: T[];
      renderItem: (item: T) => React.ReactNode;
    }
    
    function List<T>({ items, renderItem }: ListProps<T>) {
      return <ul>{items.map(renderItem)}</ul>;
    }
  • Constrain generics to specific shapes with extends.
    interface HasId {
      id: string;
    }
    
    function useItem<T extends HasId>(item: T) {
      return item.id;
    }
  • Use generic components for select or dropdown inputs.
    interface SelectProps<T> {
      options: T[];
      getLabel: (option: T) => string;
      onChange: (selected: T) => void;
    }
    
    function Select<T>({ options, getLabel, onChange }: SelectProps<T>) {
      return (
        <select onChange={(e) => onChange(options[Number(e.target.value)])}>
          {options.map((opt, i) => (
            <option key={i} value={i}>{getLabel(opt)}</option>
          ))}
        </select>
      );
    }
  • Pass default type parameters to reduce boilerplate for common cases.
    interface TableProps<T = Record<string, unknown>> {
      rows: T[];
      columns: Array<{ key: keyof T; label: string }>;
    }
  • Use React.forwardRef with generics for typed ref-forwarding components.
    const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
      (props, ref) => <input ref={ref} {...props} />
    );

Component Composition

  • Type children prop explicitly for wrapper components.
    interface WrapperProps {
      children: React.ReactNode;
    }
    
    function Wrapper({ children }: WrapperProps) {
      return <div>{children}</div>;
    }
  • Create typed render prop patterns for flexible data injection.
    interface DataFetcherProps<T> {
      render: (data: T) => React.ReactNode;
      url: string;
    }
    
    function DataFetcher<T>({ render, url }: DataFetcherProps<T>) {
      const data = useFetch<T>(url);
      return <>{render(data)}</>;
    }
  • Use React.PropsWithChildren to add children to any props type.
    function Container<T>(props: React.PropsWithChildren<{
      data: T;
    }>) {
      return <div>{props.children}</div>;
    }
  • Compose higher-order component types with TypeScript generics.
    function withAuth<P extends object>(Component: React.ComponentType<P>) {
      return function AuthWrapped(props: P) {
        const { user } = useAuth();
        if (!user) return <Redirect to="/login" />;
        return <Component {...props} />;
      };
    }
  • Use React.ElementType to accept any component or tag as a prop.
    interface BoxProps {
      as?: React.ElementType;
      children: React.ReactNode;
    }
    
    function Box({ as: Tag = "div", children }: BoxProps) {
      return <Tag>{children}</Tag>;
    }

Tip: Export prop interfaces so consumers can extend or override them when needed for customization.

Warning: Avoid using any type — specific types catch bugs at compile time and make refactoring safer.

TypeScript With React Patterns