TypeScript brings type safety to React applications. Let’s explore how to use TypeScript effectively in React projects.
Defining Props Interface
interface ButtonProps {
text: string;
onClick: () => void;
variant?: "primary" | "secondary";
disabled?: boolean;
}
const Button: React.FC<ButtonProps> = ({
text,
onClick,
variant = "primary",
disabled = false,
}) => {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{text}
</button>
);
};
State Management with TypeScript
useState with Types
import React, { useState } from "react";
interface User {
id: number;
name: string;
email: string;
}
const UserProfile: React.FC = () => {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const fetchUser = async (id: number) => {
setLoading(true);
try {
const response = await fetch(`/api/users/${id}`);
const userData: User = await response.json();
setUser(userData);
} catch (error) {
console.error("Error fetching user:", error);
} finally {
setLoading(false);
}
};
return (
<div>
{loading ? (
<p>Loading...</p>
) : user ? (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
) : (
<p>No user data</p>
)}
</div>
);
};
useReducer with TypeScript
import React, { useReducer } from "react";
interface TodoState {
todos: Todo[];
filter: "all" | "completed" | "active";
}
interface Todo {
id: number;
text: string;
completed: boolean;
}
type TodoAction =
| { type: "ADD_TODO"; payload: string }
| { type: "TOGGLE_TODO"; payload: number }
| { type: "SET_FILTER"; payload: TodoState["filter"] };
const todoReducer = (state: TodoState, action: TodoAction): TodoState => {
switch (action.type) {
case "ADD_TODO":
return {
...state,
todos: [
...state.todos,
{ id: Date.now(), text: action.payload, completed: false },
],
};
case "TOGGLE_TODO":
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo,
),
};
case "SET_FILTER":
return {
...state,
filter: action.payload,
};
default:
return state;
}
};
Event Handling
import React, { ChangeEvent, FormEvent } from "react";
const ContactForm: React.FC = () => {
const [formData, setFormData] = useState({
name: "",
email: "",
message: "",
});
const handleInputChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
};
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log("Form submitted:", formData);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="name"
value={formData.name}
onChange={handleInputChange}
placeholder="Your name"
/>
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
placeholder="Your email"
/>
<textarea
name="message"
value={formData.message}
onChange={handleInputChange}
placeholder="Your message"
/>
<button type="submit">Send</button>
</form>
);
};
Generic Components
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item) => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// Usage
const users: User[] = [
{ id: 1, name: "John", email: "john@example.com" },
{ id: 2, name: "Jane", email: "jane@example.com" },
];
<List
items={users}
renderItem={(user) => (
<span>
{user.name} ({user.email})
</span>
)}
keyExtractor={(user) => user.id}
/>;
Custom Hooks with TypeScript
import { useState, useEffect } from "react";
interface FetchState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
function useFetch<T>(url: string): FetchState<T> {
const [state, setState] = useState<FetchState<T>>({
data: null,
loading: true,
error: null,
});
useEffect(() => {
const fetchData = async () => {
try {
setState((prev) => ({ ...prev, loading: true }));
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: T = await response.json();
setState({ data, loading: false, error: null });
} catch (error) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error.message : "An error occurred",
});
}
};
fetchData();
}, [url]);
return state;
}
Best Practices
1. Use Strict TypeScript Configuration
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
2. Prefer Type over Interface for Unions
// Good for unions
type Status = "loading" | "success" | "error";
// Good for object shapes
interface User {
id: number;
name: string;
email: string;
}
3. Use Utility Types
// Partial for optional properties
const updateUser = (id: number, updates: Partial<User>) => {
// Implementation
};
// Pick for selecting specific properties
type UserPreview = Pick<User, "id" | "name">;
// Omit for excluding properties
type CreateUserRequest = Omit<User, "id">;
Conclusion
TypeScript significantly improves the React development experience by providing:
- Compile-time error detection
- Better IDE support with autocomplete
- Self-documenting code through types
- Easier refactoring and maintenance
By following these best practices, you’ll write more reliable and maintainable React applications with TypeScript! 🚀
