Back to Blog
TypeScript16 min read

TypeScript with React: Best Practices and Patterns

TypeScript brings type safety to React applications, catching errors at compile time and improving developer experience. In this guide, we'll explore best practices and patterns for using TypeScript effectively in React applications.

TypeScript brings type safety to React applications, catching errors at compile time and improving developer experience. In this guide, we'll explore best practices and patterns for using TypeScript effectively in React applications.

Type Definitions for Props

Defining component prop types:

// Define types
type Product = {
  id: string | number;
  name: string;
  price: number;
  stock: number;
  categoryId: number;
  categoryName?: string;
};

// Component with typed props
interface ProductCardProps {
  product: Product;
  onEdit?: (id: string | number) => void;
  onDelete?: (id: string | number) => void;
  showActions?: boolean;
}

function ProductCard({ 
  product, 
  onEdit, 
  onDelete, 
  showActions = true 
}: ProductCardProps) {
  return (
    <div className="product-card">
      <h3>{product.name}</h3>
      <p>${product.price}</p>
      <p>Stock: {product.stock}</p>
      {showActions && (
        <div>
          {onEdit && <button onClick={() => onEdit(product.id)}>Edit</button>}
          {onDelete && <button onClick={() => onDelete(product.id)}>Delete</button>}
        </div>
      )}
    </div>
  );
}

Typed Hooks

Creating custom hooks with proper types:

import { useState, useEffect } from "react";
import { useGetProductsQuery } from "../../state/products/productSlice";

type UseProductsReturn = {
  products: Product[];
  isLoading: boolean;
  isError: boolean;
  error: any;
  refetch: () => void;
};

function useProducts(): UseProductsReturn {
  const { data, isLoading, isError, error, refetch } = useGetProductsQuery({});
  
  return {
    products: data?.data || [],
    isLoading,
    isError,
    error,
    refetch,
  };
}

// Usage
function ProductsPage() {
  const { products, isLoading } = useProducts();
  
  if (isLoading) return <div>Loading...</div>;
  
  return (
    <div>
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

Forward Refs

Typing forwardRef components:

import React, { forwardRef } from "react";

type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
  label: string;
  error?: string;
  required?: boolean;
};

const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ label, error, required = false, ...rest }, ref) => {
    return (
      <div>
        <label>
          {label}
          {required && <span className="text-red-500">*</span>}
        </label>
        <input
          ref={ref}
          {...rest}
          className={`input ${error ? "error" : ""}`}
        />
        {error && <p className="text-red-500 text-xs">{error}</p>}
      </div>
    );
  }
);

Input.displayName = "Input";
export default Input;

Event Handlers

Typing event handlers:

function ProductForm() {
  const [name, setName] = useState<string>("");
  
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // Handle submit
  };
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setName(e.target.value);
  };
  
  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    e.preventDefault();
    // Handle click
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={name}
        onChange={handleChange}
      />
      <button type="submit" onClick={handleClick}>
        Submit
      </button>
    </form>
  );
}

Generic Components

interface SelectProps<T> {
  options: { value: T; label: string }[];
  value: T;
  onChange: (value: T) => void;
  placeholder?: string;
}

function Select<T extends string | number>({
  options,
  value,
  onChange,
  placeholder,
}: SelectProps<T>) {
  return (
    <select
      value={value}
      onChange={(e) => onChange(e.target.value as T)}
    >
      {placeholder && <option value="">{placeholder}</option>}
      {options.map((option) => (
        <option key={option.value} value={option.value}>
          {option.label}
        </option>
      ))}
    </select>
  );
}

// Usage
<Select<string>
  options={[{ value: "pcs", label: "Pieces" }, { value: "kg", label: "Kilograms" }]}
  value={unit}
  onChange={setUnit}
/>

Type Utilities

// Extract types from API responses
type ProductResponse = {
  success: boolean;
  data: Product[];
};

type Product = ProductResponse["data"][number];

// Partial types
type PartialProduct = Partial<Product>;

// Pick and Omit
type ProductPreview = Pick<Product, "id" | "name" | "price">;
type ProductWithoutId = Omit<Product, "id">;

// Required fields
type RequiredProduct = Required<Product>;

// Record types
type ProductStatus = Record<string, "active" | "inactive" | "pending">;

Best Practices

  • Always type component props explicitly
  • Use interfaces for object shapes, types for unions
  • Leverage type inference where possible
  • Use type utilities (Pick, Omit, Partial) effectively
  • Avoid using 'any' - use 'unknown' instead
  • Create shared type definitions for consistency
  • Use const assertions for literal types

Conclusion

TypeScript enhances React development by providing type safety, better IDE support, and catching errors early. Following these best practices ensures maintainable, scalable React applications with excellent developer experience. The patterns shown here are used throughout modern React applications and inventory management systems.