Back to Blog
Tools & Services16 min read

TypeScript with React – Best Practices

Master TypeScript best practices for building type-safe React applications. Complete guide with type definitions, interfaces, generics, hooks, and patterns. Learn with examples.

When I first started using TypeScript with React, I thought it would just add extra typing overhead. Boy, was I wrong. TypeScript has saved me countless hours by catching bugs before they reach production, and it's made my codebase infinitely more maintainable. But using TypeScript effectively in React requires understanding some patterns and best practices that aren't always obvious.

TypeScript brings compile-time type checking to React, which means you catch errors while writing code, not when users report bugs. It also provides incredible IDE support—autocomplete, refactoring, and navigation all work better when TypeScript understands your code structure. But to get these benefits, you need to type your components, props, hooks, and event handlers correctly.

In this guide, I'll share the TypeScript patterns I use in production React applications. We'll cover typing component props (including children and refs), typing hooks (useState, useEffect, custom hooks), typing event handlers (onClick, onChange, form events), using generics for reusable components, and leveraging TypeScript utility types. I'll also share some gotchas I've encountered that can trip you up if you're not careful.

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.