DEV

TypeScript Patterns Every React Developer Should Know

/ ARTICLE

TypeScript and React are a natural fit — until they aren’t. Props that should be flexible become rigid, generic components fight the type checker, and as any starts creeping into your codebase. The good news: a handful of patterns will cover most of the friction. Let’s walk through the ones that have saved me countless hours.

Generic Components for Reusable UI

When building a Select or DataTable that works with any data shape, generics are essential. The goal is to infer the item type from the data prop and flow it through to render props and callbacks. Here’s a minimal Select that preserves the type of options through to onChange:

type SelectProps<T> = {
  options: T[];
  value: T;
  onChange: (value: T) => void;
  getLabel: (option: T) => string;
  getValue: (option: T) => string;
};

function Select<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) {
  return (
    <select
      value={getValue(value)}
      onChange={(e) => {
        const option = options.find((o) => getValue(o) === e.target.value);
        if (option) onChange(option);
      }}
    >
      {options.map((opt) => (
        <option key={getValue(opt)} value={getValue(opt)}>
          {getLabel(opt)}
        </option>
      ))}
    </select>
  );
}

// Usage: full type inference, no casts needed
type User = { id: string; name: string };
const users: User[] = [{ id: "1", name: "Alice" }];
<Select
  options={users}
  value={users[0]}
  onChange={(user) => console.log(user.name)}
  getLabel={(u) => u.name}
  getValue={(u) => u.id}
/>

Discriminated Unions for Component Variants

When a component has multiple “modes” with different props, discriminated unions keep things type-safe. Use a variant or mode field as the discriminator, and TypeScript narrows the rest automatically:

type ButtonProps =
  | { variant: "primary"; onClick: () => void }
  | { variant: "link"; href: string }
  | { variant: "submit"; form: string };

function Button(props: ButtonProps) {
  if (props.variant === "link") {
    return <a href={props.href}>Click me</a>;
  }
  if (props.variant === "submit") {
    return <button type="submit" form={props.form}>Submit</button>;
  }
  return <button onClick={props.onClick}>Primary</button>;
}

Type-safe React Context requires a bit of setup. Provide a default of null, and use a custom hook that throws when the context is missing. That way, you never accidentally use the context outside its provider, and consumers get proper autocomplete for the value. Combined with React.forwardRef and correct generic typing for refs, you’ll eliminate most of the @ts-ignore comments in your component library. The investment up front pays off in maintainability and fewer runtime bugs.