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.

Tips

  1. 01Export prop interfaces so consumers can extend or override them when needed for customization.

Warnings

  1. 01Avoid using the any type — specific types catch bugs at compile time and make refactoring safer.

Typing Component Props

  • Define props with an interface — use ? for optional props with defaults.

    interface ButtonProps {
      label: string;
      onClick: () => void;
      disabled?: boolean;
    }
    
    function Button({ label, onClick, disabled = false }: ButtonProps) {
      return <button onClick={onClick} disabled={disabled}>{label}</button>;
    }
    
  • Use type instead of interface when you need union or intersection props.

    type CardProps = {
      title: string;
      variant: "primary" | "secondary";
    };
    
    function Card({ title, variant }: CardProps) {
      return <div className={variant}><h2>{title}</h2></div>;
    }
    
  • Export prop interfaces so consumers can extend or build on them.

    export interface ButtonProps {
      label: string;
      onClick: () => void;
    }
    
    export function Button(props: ButtonProps) { /* ... */ }
    
  • Extend HTML element attributes to allow all native props on wrappers.

    interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
      label: string;
    }
    
    function LabeledInput({ label, ...rest }: InputProps) {
      return <label>{label}<input {...rest} /></label>;
    }
    
  • Use React.ComponentPropsWithoutRef to forward all props of a native tag.

    interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
      variant: "primary" | "ghost";
    }
    
    function Button({ variant, ...rest }: ButtonProps) {
      return <button className={variant} {...rest} />;
    }
    

Event Handlers

  • Type a change handler for text inputs using React.ChangeEvent.

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
      setName(e.target.value); // e.target.value is string
    };
    
  • Type a form submit handler using React.FormEventHandler.

    function Form() {
      const handleSubmit: React.FormEventHandler<HTMLFormElement> = (e) => {
        e.preventDefault();
        // handle form data
      };
      return <form onSubmit={handleSubmit}></form>;
    }
    
  • Type a mouse click handler using React.MouseEventHandler.

    const handleClick: React.MouseEventHandler<HTMLButtonElement> = (e) => {
      console.log(e.button); // 0 = left, 1 = middle, 2 = right
    };
    
  • Type a keyboard handler to respond to specific key presses.

    function SearchBox() {
      const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
        if (e.key === "Enter") submitSearch();
      };
      return <input onKeyDown={handleKeyDown} />;
    }
    
  • Define typed handler props in an interface for reusable form 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

  • Pass a union type to useState when the initial value can be null.

    const [count, setCount] = useState<number>(0);
    const [user, setUser] = useState<User | null>(null);
    
  • Type useRef for DOM elements and access it with optional chaining.

    const inputRef = useRef<HTMLInputElement>(null);
    
    function focus() {
      inputRef.current?.focus(); // safe — current may be null
    }
    
  • Type createContext with a default and wrap it in a typed custom hook.

    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 a discriminated union to cover every action shape.

    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;
      }
    };
    
  • Use as const on the return array to get a precise tuple type from a custom hook.

    function useToggle(initial: boolean) {
      const [value, setValue] = useState(initial);
      const toggle = () => setValue(v => !v);
      return [value, toggle] as const; // [boolean, () => void]
    }
    

Children and Refs

  • Use React.ReactNode for children props — it accepts JSX, strings, or null.

    interface WrapperProps {
      children: React.ReactNode;
    }
    
    function Wrapper({ children }: WrapperProps) {
      return <div className="wrapper">{children}</div>;
    }
    
  • Use React.PropsWithChildren to add children to any existing props type.

    interface CardProps { title: string; }
    
    function Card({ title, children }: React.PropsWithChildren<CardProps>) {
      return <div><h2>{title}</h2>{children}</div>;
    }
    
  • Use useRef with a DOM type and access .current safely with optional chaining.

    const videoRef = useRef<HTMLVideoElement>(null);
    
    function play() {
      videoRef.current?.play(); // optional chain handles null
    }
    
  • Use React.forwardRef with explicit generic types for ref-forwarding components.

    const TextInput = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
      (props, ref) => <input ref={ref} {...props} />
    );
    
  • Distinguish RefObject (read-only current) from MutableRefObject (writable current).

    // React.RefObject<T>: current is readonly — for DOM refs via useRef(null)
    const domRef: React.RefObject<HTMLDivElement> = useRef(null);
    
    // React.MutableRefObject<T>: current is writable — for storing values
    const timerRef: React.MutableRefObject<number> = useRef(0);
    

TypeScript Config for React

  • Set jsx to react-jsx to avoid importing React in every file.

    {
      "compilerOptions": {
        "jsx": "react-jsx"
      }
    }
    
  • Enable strict mode to activate all strict type checks at once.

    {
      "compilerOptions": {
        "strict": true
      }
    }
    
  • Enable noUncheckedIndexedAccess to catch undefined array access at compile time.

    {
      "compilerOptions": {
        "noUncheckedIndexedAccess": true
      }
    }
    // items[0] is now string | undefined instead of string
    
  • Use paths to create tsconfig aliases for cleaner imports across large projects.

    {
      "compilerOptions": {
        "paths": {
          "@components/*": ["./src/components/*"],
          "@lib/*": ["./src/lib/*"]
        }
      }
    }
    
  • Set composite: true to enable project references in monorepo packages.

    {
      "compilerOptions": {
        "composite": true,
        "declaration": true,
        "declarationMap": true
      }
    }
    

FAQ