TypeScript With React Patterns

Master advanced React patterns with TypeScript including discriminated unions, polymorphic components, and compound architectures.

TL;DR

  1. Model mutually exclusive prop configurations using discriminated type union structures.
  2. Build polymorphic component wrappers leveraging React.ElementType generic constraints.
  3. Share nested compound component state through strictly typed createContext providers.

Discriminated Union Props

    Variant prop union

    Model mutually exclusive component states.

    type AlertProps =
      | { variant: "ok"; text: string }
      | { variant: "err"; text: string; code: number };
    
    function Alert(p: AlertProps) {
      return <Card>{p.text}</Card>;
    }
    Discriminant property narrowing

    Access variant properties after checking.

    function renderAlert(props: AlertProps) {
      if (props.variant === "error") {
        console.log("Error code:", props.code);
      }
    }
    Exhaustive never check

    Ensure all union variants are handled.

    type Status = "idle" | "busy";
    function getLabel(s: Status): string {
      switch (s) {
        case "idle": return "Wait";
        case "busy": return "Work";
        default: {
          const _: never = s;
          return _;
        }
      }
    }

Polymorphic Components

    Generic as prop helper

    Extract native props based on ElementType.

    import type {
      ElementType,
      ComponentPropsWithRef
    } from "react";
    type AsProps<E extends ElementType> = {
      as?: E;
    } & ComponentPropsWithRef<E>;
    Polymorphic Box component

    Dynamically render element with correct props.

    import type { ElementType } from "react";
    function Box<E extends ElementType = "button">(
      { as, ...rest }: AsProps<E>
    ) {
      const Component = as ?? "button";
      return <Component {...rest} />;
    }
    Type safe polymorphic invocation

    TypeScript narrows valid element attributes.

    // Valid: button allows onClick and disabled
    const btn = <Box as="button" disabled={true} />;

Forward Ref and Handles

    Imperative handle interface

    Define methods callable via component ref.

    export interface ModalHandle {
      open: () => void;
      close: () => void;
    }
    Implementing useImperativeHandle

    Expose handle methods instead of raw DOM.

    import {
      forwardRef, useImperativeHandle
    } from "react";
    const Modal = forwardRef<
      ModalHandle, { title: string }
    >(({ title }, ref) => {
      useImperativeHandle(ref, () => ({
        open: () => {}, close: () => {}
      }));
      return <Card>{title}</Card>;
    });
    Calling imperative handle

    Invoke exposed handle methods from parent.

    import { useRef } from "react";
    function Page() {
      const ref = useRef<ModalHandle>(null);
      return (
        <Button onClick={() => ref.current?.open()} />
      );
    }

Compound Components Pattern

    Compound component context

    Share parent state with child components.

    import { createContext } from "react";
    interface TabsCtx {
      activeId: string;
      setActive: (id: string) => void;
    }
    const Ctx = createContext<TabsCtx | null>(null);
    Parent container provider

    Wrap children in compound context provider.

    import type { ReactNode } from "react";
    function Tabs({ children }: { children: ReactNode }) {
      const [activeId, setActiveId] = useState("tab1");
      return (
        <Ctx.Provider value={{ activeId, setActiveId }}>
          <Container>{children}</Container>
        </Ctx.Provider>
      );
    }
    Child component hook consumer

    Consume parent context inside compound child.

    interface TabProps { id: string; label: string; }
    function TabItem({ id, label }: TabProps) {
      const ctx = useContext(Ctx);
      if (!ctx) throw new Error("TabItem must be in Tabs");
      return (
        <Button onClick={() => ctx.setActiveId(id)}>
          {label}
        </Button>
      );
    }
    Attaching static properties

    Group subcomponents under parent namespace.

    Tabs.Item = TabItem;
    // Usage:
    // <Tabs><Tabs.Item id="t1" label="Home" /></Tabs>

Render Props and Custom Hooks

    Render prop pattern

    Pass data to render callbacks cleanly.

    interface FetcherProps<T> {
      url: string;
      render: (
        data: T,
        loading: boolean
      ) => React.ReactNode;
    }
    function Fetcher<T>({ url, render }: FetcherProps<T>) {
      // data fetching logic here
      return null;
    }
    Typed tuple return

    Mark tuple returns with as const to preserve types.

    function useToggle(initial = false) {
      const [open, setOpen] = useState(initial);
      const toggle = () => setOpen(prev => !prev);
      return [open, toggle] as const;
    }
    Generic custom hook

    Infer hook payload data types using generic parameters.

    function useFetch<T>(url: string): {
      data: T | null;
      loading: boolean;
      error: Error | null;
    } {
      return { data: null, loading: false, error: null };
    }

Tips

  1. Utilize an exhaustive switch block with a never fallback check when rendering discriminated union props to ensure missing variants trigger compile errors.
  2. Return custom hook arrays typed with as const to produce strongly typed tuples rather than broadened array unions.

Warnings

  1. Avoid using as any when implementing polymorphic as components; allow generics to propagate native element attributes safely.
  2. Remember to wrap child elements using <Fragment> or capitalized components rather than raw lowercase tags to ensure clean JSX syntax separation.

In Practice

FAQ