Hero image for TypeScript Tips for React Developers

TypeScript Tips for React Developers

TypeScriptReactFrontend

TypeScript has become the standard for building large-scale React applications. Here are some patterns I’ve found invaluable in my daily work.

Typing Component Props

Basic Props Interface

interface ButtonProps {
  variant: "primary" | "secondary" | "outline";
  size?: "sm" | "md" | "lg";
  children: React.ReactNode;
  onClick?: () => void;
}

function Button({ variant, size = "md", children, onClick }: ButtonProps) {
  return (
    <button className={`btn btn-${variant} btn-${size}`} onClick={onClick}>
      {children}
    </button>
  );
}

Extending HTML Element Props

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
  label: string;
  error?: string;
}

function Input({ label, error, ...props }: InputProps) {
  return (
    <div>
      <label>{label}</label>
      <input {...props} />
      {error && <span className="error">{error}</span>}
    </div>
  );
}

Generic Components

Create flexible, reusable components with generics:

interface 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 selected = options.find(
          (opt) => getValue(opt) === e.target.value
        );
        if (selected) onChange(selected);
      }}
    >
      {options.map((option) => (
        <option key={getValue(option)} value={getValue(option)}>
          {getLabel(option)}
        </option>
      ))}
    </select>
  );
}

Discriminated Unions for State

Handle complex state with discriminated unions:

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

function useAsync<T>(): [AsyncState<T>, (promise: Promise<T>) => void] {
  const [state, setState] = useState<AsyncState<T>>({ status: "idle" });

  const execute = async (promise: Promise<T>) => {
    setState({ status: "loading" });
    try {
      const data = await promise;
      setState({ status: "success", data });
    } catch (error) {
      setState({ status: "error", error: error as Error });
    }
  };

  return [state, execute];
}

Utility Types You Should Know

  • Partial<T> - Makes all properties optional
  • Required<T> - Makes all properties required
  • Pick<T, K> - Select specific properties
  • Omit<T, K> - Remove specific properties
  • Record<K, V> - Create an object type with specific keys

Conclusion

TypeScript adds a learning curve, but the benefits in code quality and developer experience are substantial. Start with these patterns and gradually adopt more advanced techniques as you become comfortable.