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.
Related Articles
React Hook Form with Zod Validation: Complete Guide
Learn how to implement form validation in React using React Hook Form and Zod with TypeScript.
TanStack Table Implementation in React: Complete Guide
Build advanced data tables with TypeScript, sorting, filtering, and pagination.
React Router Setup: Complete Guide for React Applications
Learn how to set up React Router DOM with TypeScript for type-safe routing.
Next.js Caching and Rendering: A Complete Guide for 2026
Master Next.js caching strategies and rendering patterns for optimal performance.