Back to Blog
ReactTypeScriptFrontendRTK QueryMaterial UI

React + TypeScript: Patterns That Actually Matter in Production

2024-05-10·7 min read

Why TypeScript Over Plain JavaScript?

After working on several large React codebases in both JavaScript and TypeScript, the verdict is clear: TypeScript catches a category of bugs that unit tests never will. Especially in large teams, the type system is the fastest way to communicate intent and prevent regressions.

Typing API Responses with RTK Query

When using Redux Toolkit's RTK Query, always type your endpoints:

interface Device {
  id: string;
  hostname: string;
  status: "online" | "offline" | "pending";
  lastSeen: string;
  tenantId: string;
}

const deviceApi = createApi({
  reducerPath: "deviceApi",
  baseQuery: fetchBaseQuery({ baseUrl: "/api" }),
  endpoints: (builder) => ({
    getDevices: builder.query<Device[], string>({
      query: (tenantId) => `/devices?tenant_id=${tenantId}`,
    }),
    updateDevice: builder.mutation<Device, Partial<Device> & { id: string }>({
      query: ({ id, ...body }) => ({
        url: `/devices/${id}`,
        method: "PATCH",
        body,
      }),
    }),
  }),
});

Generic Components with TypeScript

Generic components let you build reusable UI with full type safety:

interface TableProps<T> {
  data: T[];
  columns: Column<T>[];
  onRowClick?: (row: T) => void;
  loading?: boolean;
}

function DataTable<T extends { id: string }>({
  data,
  columns,
  onRowClick,
  loading = false,
}: TableProps<T>) {
  // Component implementation
}

Discriminated Unions for State Management

Use discriminated unions to model component states explicitly:

type AsyncState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: string };

function useDeviceSync(tenantId: string): AsyncState<Device[]> {
  const { data, isLoading, error } = useGetDevicesQuery(tenantId);

  if (isLoading) return { status: "loading" };
  if (error) return { status: "error", error: "Failed to load devices" };
  if (data) return { status: "success", data };
  return { status: "idle" };
}

Component Props Patterns

Prefer explicit prop interfaces over inline types:

// ✅ Good — named interface, easy to reuse and extend
interface DeviceCardProps {
  device: Device;
  onAction: (action: DeviceAction) => void;
  compact?: boolean;
}

// ❌ Avoid — inline types for complex props
function DeviceCard({
  device,
  onAction,
}: {
  device: Device;
  onAction: (action: DeviceAction) => void;
}) {}

Strict Mode Configuration

Always enable strict TypeScript settings:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
  }
}

Conclusion

TypeScript's value compounds over time. The initial friction of writing types pays off exponentially in a 6-month-old codebase where you've forgotten what a function returns, or in a PR review where the type error catches a bug before it reaches production.

Start strict from day one — retrofitting TypeScript into a large JavaScript codebase is painful.