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.
📖 Want the complete guide with more examples and advanced patterns? Check out the full article on my blog for an in-depth tutorial with additional code examples, troubleshooting tips, and real-world use cases.
Why TypeScript with React?
TypeScript with React provides:
- Type safety - Catch errors at compile time
- Better IDE support - Autocomplete, IntelliSense, and refactoring
- Self-documenting code - Types serve as documentation
- Easier refactoring - TypeScript catches breaking changes
- Better collaboration - Types help team members understand code
- Runtime error prevention - Catch bugs before they reach production
Type Definitions for Props
Define component prop types using interfaces or 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>
);
}
Type vs Interface
- Interfaces - Better for object shapes, extendable, can be merged
- Types - Better for unions, intersections, and computed types
// Interface - good for component props
interface ButtonProps {
label: string;
onClick: () => void;
}
// Type - good for unions
type Status = "loading" | "success" | "error";
// Type - good for intersections
type ButtonWithIcon = ButtonProps & {
icon: React.ReactNode;
};
Typing React Hooks
useState Hook
Type useState explicitly or let TypeScript infer:
// Explicit typing
const [name, setName] = useState<string>("");
const [count, setCount] = useState<number>(0);
const [products, setProducts] = useState<Product[]>([]);
// Type inference (TypeScript infers from initial value)
const [isLoading, setIsLoading] = useState(false); // boolean
const [user, setUser] = useState(null); // null - need to type this
const [user, setUser] = useState<User | null>(null); // Better
useEffect Hook
useEffect is automatically typed, but you can type dependencies:
useEffect(() => {
// Effect logic
}, [dependency1, dependency2]); // TypeScript checks dependencies
Custom Hooks
Create typed custom hooks with explicit return 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
Type forwardRef components properly:
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
Type event handlers using React's built-in event types:
function ProductForm() {
const [name, setName] = useState<string>("");
const [price, setPrice] = useState<number>(0);
// Form submit event
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Handle submit
};
// Input change event
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
};
// Number input change
const handlePriceChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setPrice(Number(e.target.value));
};
// Button click event
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
// Handle click
};
// Select change event
const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = e.target.value;
// Handle select change
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={handleNameChange}
/>
<input
type="number"
value={price}
onChange={handlePriceChange}
/>
<button type="submit" onClick={handleClick}>
Submit
</button>
</form>
);
}
Common Event Types
-
React.FormEvent<HTMLFormElement>- Form submission -
React.ChangeEvent<HTMLInputElement>- Input changes -
React.ChangeEvent<HTMLSelectElement>- Select changes -
React.ChangeEvent<HTMLTextAreaElement>- Textarea changes -
React.MouseEvent<HTMLButtonElement>- Button clicks -
React.KeyboardEvent<HTMLInputElement>- Keyboard events
Generic Components
Create reusable generic components with TypeScript generics:
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 with string
<Select<string>
options={[{ value: "pcs", label: "Pieces" }, { value: "kg", label: "Kilograms" }]}
value={unit}
onChange={setUnit}
/>
// Usage with number
<Select<number>
options={[{ value: 1, label: "One" }, { value: 2, label: "Two" }]}
value={selectedNumber}
onChange={setSelectedNumber}
/>
TypeScript Utility Types
Leverage TypeScript utility types for type transformations:
// Extract types from API responses
type ProductResponse = {
success: boolean;
data: Product[];
};
type Product = ProductResponse["data"][number];
// Partial - makes all properties optional
type PartialProduct = Partial<Product>;
// { id?: string; name?: string; price?: number; ... }
// Required - makes all properties required
type RequiredProduct = Required<Product>;
// { id: string; name: string; price: number; ... }
// Pick - select specific properties
type ProductPreview = Pick<Product, "id" | "name" | "price">;
// { id: string; name: string; price: number; }
// Omit - exclude specific properties
type ProductWithoutId = Omit<Product, "id">;
// { name: string; price: number; stock: number; ... }
// Record - create object types
type ProductStatus = Record<string, "active" | "inactive" | "pending">;
// { [key: string]: "active" | "inactive" | "pending" }
// Extract - extract types from unions
type Status = "loading" | "success" | "error";
type SuccessStatus = Extract<Status, "success">; // "success"
// Exclude - exclude types from unions
type NonErrorStatus = Exclude<Status, "error">; // "loading" | "success"
Using Utility Types in Components
// Create a form component that accepts partial product data
interface ProductFormProps {
product?: Partial<Product>;
onSubmit: (data: Omit<Product, "id">) => void;
}
function ProductForm({ product, onSubmit }: ProductFormProps) {
// Form implementation
}
// Create a preview component that only needs specific fields
interface ProductPreviewProps {
product: Pick<Product, "id" | "name" | "price">;
}
function ProductPreview({ product }: ProductPreviewProps) {
return (
<div>
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
);
}
Typing Children
Type React children properly:
// Using React.ReactNode (most common)
interface CardProps {
title: string;
children: React.ReactNode;
}
function Card({ title, children }: CardProps) {
return (
<div>
<h2>{title}</h2>
{children}
</div>
);
}
// Using React.ReactElement for single element
interface ButtonGroupProps {
children: React.ReactElement<ButtonProps>[];
}
// Using specific element type
interface LayoutProps {
children: React.ReactElement;
}
Typing Context
Type React Context properly:
interface AuthContextType {
user: User | null;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = React.createContext<AuthContextType | undefined>(undefined);
function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const value: AuthContextType = {
user,
isAuthenticated: !!user,
login: async (email, password) => {
// Login logic
},
logout: () => {
setUser(null);
},
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
// Custom hook with type safety
function useAuth(): AuthContextType {
const context = React.useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within AuthProvider");
}
return context;
}
Best Practices
- Always type component props explicitly - Don't rely on inference for props
- Use interfaces for object shapes - More traditional for React props
- Use types for unions and intersections - Better for complex types
- Leverage type inference where possible - Let TypeScript infer simple types
- Use type utilities effectively - Pick, Omit, Partial for type transformations
- Avoid using 'any' - Use 'unknown' instead when type is truly unknown
- Create shared type definitions - Reuse types across components
- Use const assertions - For literal types and readonly data
- Type event handlers explicitly - Use React's built-in event types
- Use generics for reusable components - Make components type-safe and reusable
Avoiding Common Mistakes
// ❌ Bad - using 'any'
function Component(props: any) {
return <div>{props.name}</div>;
}
// ✅ Good - explicit typing
interface ComponentProps {
name: string;
}
function Component(props: ComponentProps) {
return <div>{props.name}</div>;
}
// ❌ Bad - not typing useState properly
const [user, setUser] = useState(null);
// ✅ Good - explicit typing
const [user, setUser] = useState<User | null>(null);
// ❌ Bad - using 'any' for events
const handleChange = (e: any) => {
setName(e.target.value);
};
// ✅ Good - using proper event types
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
};
Resources and Further Reading
- 📚 Full TypeScript React Guide - Complete tutorial with advanced examples, troubleshooting, and best practices
- React Hook Form with Zod - Type-safe form validation
- TanStack Table Guide - TypeScript data tables
- React Router Guide - Type-safe routing
- TypeScript Handbook - Official TypeScript documentation
- React TypeScript Cheatsheet - Comprehensive React TypeScript guide
- TypeScript Utility Types - Official utility types docs
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.
Key Takeaways:
- TypeScript provides compile-time type checking for React
- Type component props explicitly using interfaces or types
- Type hooks properly, especially custom hooks
- Use React's event types for event handlers
- Leverage generics for reusable components
- Use utility types for type transformations
- Avoid 'any' - use 'unknown' when needed
- Create shared types for consistency across components
Whether you're building a simple component or a complex application, TypeScript provides the type safety you need. It catches errors before they reach production and makes your codebase more maintainable.
What's your experience with TypeScript and React? Share your tips and tricks in the comments below! 🚀
💡 Looking for more details? This is a condensed version of my comprehensive guide. Read the full article on my blog for additional examples, advanced patterns, troubleshooting tips, and more in-depth explanations.
If you found this guide helpful, consider checking out my other articles on React development and TypeScript best practices.
Top comments (0)