Back to TypeScript tutorials
Intermediate15 min read

TypeScript with React

Type React components, hooks, events, and context for safe, refactor-friendly UI code.

Typing Component Props

Define props with interfaces or type aliases: `interface ButtonProps { label: string; onClick: () => void; disabled?: boolean }`. Function components type props as the first generic parameter: `function Button({ label }: ButtonProps)`.

Extend native element props with `ComponentPropsWithoutRef<"button">` and intersect custom props. Use `React.PropsWithChildren` or explicit `children?: ReactNode` for components accepting children.

  • Interface for props contract
  • Extend native element props with utility types
  • Explicit children typing
interface ButtonProps extends React.ComponentPropsWithoutRef<'button'> {
  variant?: 'primary' | 'secondary';
}

Events and Refs

DOM events use specific types: `React.ChangeEvent<HTMLInputElement>`, `React.MouseEvent<HTMLButtonElement>`. Generic parameter matches the element type for correct target typing.

Refs use `useRef<HTMLInputElement>(null)` and check null before access. `forwardRef` components type the ref parameter with `React.forwardRef<HTMLInputElement, Props>`.

  • Specific event types per element
  • Ref generics with null checks
  • forwardRef for ref-forwarding components
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  setValue(e.target.value);
};

Hooks Typing

useState infers from initial value — provide explicit generic when initial value is null: `useState<User | null>(null)`. useReducer types state and action union. useContext requires typed context created with `createContext<T | null>(null)` and null checks at consumption.

Custom hooks return typed tuples or objects — export return types for consumers. Generic custom hooks (`function useLocalStorage<T>`) preserve value types through serialization boundaries when combined with runtime validation.

  • Explicit useState generic for nullable init
  • Typed context with null guard
  • Generic custom hooks for reusable logic
function useUser(id: string) {
  const [user, setUser] = useState<User | null>(null);
  // ...
  return { user, loading } as const;
}

Server and Client Components

Next.js Server Components are async functions returning JSX — type props as plain interfaces without hooks. Client Components need `"use client"` and follow standard React typing.

Shared types live in non-component modules importable from both server and client. Never import server-only modules into client components — TypeScript project references and eslint boundaries help enforce separation.

  • Server Components: async, no hooks
  • Shared types in neutral modules
  • Enforce server/client import boundaries

Third-Party Components and Generics

Libraries like Radix, MUI, and TanStack Table ship excellent types — leverage them rather than casting. When wrapping third-party components, re-export narrowed prop types for your design system.

Polymorphic components (`as` prop) use generics and conditional types — advanced pattern for design systems. Start with fixed element types and add polymorphism only when multiple HTML elements are required.

  • Leverage library shipped types
  • Wrap and narrow props for design systems
  • Polymorphic components are advanced — defer until needed

Get In Touch


Ready to discuss your next project? Drop me a message.