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.