Combining React with TypeScript has become the industry standard for building robust, maintainable, and scalable web applications. When used effectively, TypeScript can dramatically improve your development experience and code quality.
In this comprehensive guide, we'll explore the best practices for using TypeScript with React to create applications that are not only type-safe but also easier to maintain and scale as your project grows in complexity.
Why TypeScript with React?
TypeScript brings static typing to JavaScript, which provides several benefits for React development:
- Early error detection: Catch errors during development rather than at runtime
- Improved developer experience: Better autocompletion, navigation, and refactoring tools
- Self-documenting code: Types serve as documentation for your components and functions
- Easier refactoring: Confidence when making changes to large codebases
- Better collaboration: Clear contracts between different parts of your application
"TypeScript is like a seatbelt for your codebase—it might feel restrictive at first, but it saves you from serious injuries when things go wrong."
1. Properly Typing Component Props
The foundation of TypeScript in React is properly typing your component props. Here are the best practices:
Use Interfaces for Props
Prefer interfaces over type aliases for component props as they are more extensible:
interface UserCardProps {
id: number;
name: string;
email: string;
role: 'admin' | 'user' | 'moderator';
isActive: boolean;
onStatusChange: (userId: number, isActive: boolean) => void;
createdAt?: Date; // Optional property
}
const UserCard: React.FC<UserCardProps> = ({
id,
name,
email,
role,
isActive,
onStatusChange,
createdAt
}) => {
return (
<div className="user-card">
<h3>{name}</h3>
<p>{email}</p>
<span className={`role-badge ${role}`}>{role}</span>
<button
onClick={() => onStatusChange(id, !isActive)}
className={isActive ? 'active' : 'inactive'}
>
{isActive ? 'Deactivate' : 'Activate'}
</button>
{createdAt && (
<p>Joined: {createdAt.toLocaleDateString()}</p>
)}
</div>
);
};
Define Default Props Properly
For components with default props, use the following pattern:
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
onClick: () => void;
children: React.ReactNode;
}
const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'medium',
disabled = false,
onClick,
children
}) => {
return (
<button
className={`btn btn-${variant} btn-${size} ${disabled ? 'disabled' : ''}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};
2. Typing Component State
Properly typing your component state is crucial for avoiding runtime errors:
Use Type Inference When Possible
For simple state, let TypeScript infer the type:
const [count, setCount] = useState(0); // Type inferred as number
const [name, setName] = useState(''); // Type inferred as string
const [isLoading, setIsLoading] = useState(false); // Type inferred as boolean
Explicitly Type Complex State
For more complex state objects, explicitly define the type:
interface User {
id: number;
name: string;
email: string;
avatar?: string;
}
interface AuthState {
user: User | null;
isAuthenticated: boolean;
isLoading: boolean;
error: string | null;
}
const AuthComponent: React.FC = () => {
const [authState, setAuthState] = useState<AuthState>({
user: null,
isAuthenticated: false,
isLoading: false,
error: null
});
// Component logic...
};
3. Typing Events
Properly typing events ensures you access the correct properties:
const InputField: React.FC = () => {
const [value, setValue] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// Submit logic...
};
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
// Click logic...
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={value}
onChange={handleChange}
/>
<button type="submit" onClick={handleClick}>
Submit
</button>
</form>
);
};
4. Creating Custom Hooks with TypeScript
Custom hooks benefit greatly from TypeScript's type system:
interface UseApiResult<T> {
data: T | null;
loading: boolean;
error: string | null;
refetch: () => void;
}
function useApi<T>(url: string): UseApiResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
}, [url]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
// Usage
interface User {
id: number;
name: string;
email: string;
}
const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
const { data: user, loading, error } = useApi<User>(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
};
5. Advanced Patterns: Generic Components
Create flexible, reusable components with generics:
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string | number;
emptyMessage?: string;
}
function List<T>({
items,
renderItem,
keyExtractor,
emptyMessage = "No items found"
}: ListProps<T>) {
if (items.length === 0) {
return <div className="empty-state">{emptyMessage}</div>;
}
return (
<ul className="list">
{items.map(item => (
<li key={keyExtractor(item)}>
{renderItem(item)}
</li>
))}
</ul>
);
}
// Usage with different data types
interface Product {
id: number;
name: string;
price: number;
}
interface User {
userId: string;
firstName: string;
lastName: string;
}
const ProductList: React.FC<{ products: Product[] }> = ({ products }) => {
return (
<List
items={products}
keyExtractor={(product) => product.id}
renderItem={(product) => (
<div>
<h3>{product.name}</h3>
<p>${product.price.toFixed(2)}</p>
</div>
)}
/>
);
};
const UserList: React.FC<{ users: User[] }> = ({ users }) => {
return (
<List
items={users}
keyExtractor={(user) => user.userId}
renderItem={(user) => (
<div>
<h3>{user.firstName} {user.lastName}</h3>
</div>
)}
/>
);
};
6. Organizing Types in Large Applications
For large applications, organize your types effectively:
Use a Centralized Types Directory
// types/index.ts
export * from './user';
export * from './product';
export * from './api';
Create Domain-Specific Type Files
// types/user.ts
export interface User {
id: number;
name: string;
email: string;
role: UserRole;
createdAt: Date;
updatedAt: Date;
}
export type UserRole = 'admin' | 'user' | 'moderator';
export interface UserCreateRequest {
name: string;
email: string;
password: string;
role?: UserRole;
}
export interface UserUpdateRequest {
name?: string;
email?: string;
role?: UserRole;
}
7. Testing with TypeScript
Leverage TypeScript in your tests for better reliability:
// Component test example
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders with correct text and handles click', () => {
const handleClick = jest.fn();
render(
<Button variant="primary" onClick={handleClick}>
Click me
</Button>
);
const button = screen.getByText('Click me');
expect(button).toBeInTheDocument();
fireEvent.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('applies correct classes based on props', () => {
const { container } = render(
<Button variant="danger" size="large" disabled>
Delete
</Button>
);
const button = container.firstChild;
expect(button).toHaveClass('btn-danger');
expect(button).toHaveClass('btn-large');
expect(button).toHaveClass('disabled');
});
});
Conclusion
TypeScript and React are a powerful combination that can significantly improve your development experience and application quality. By following these best practices, you'll create applications that are more robust, maintainable, and scalable.
Remember that TypeScript is a tool to help you, not hinder you. Start with simple type annotations and gradually adopt more advanced patterns as your comfort level grows. The investment in learning TypeScript will pay dividends throughout your React development career.
At Yukon Gold Exclusive, we incorporate TypeScript throughout our curriculum to ensure our graduates are equipped with the skills needed for modern React development. Whether you're just starting or looking to level up your skills, understanding how to effectively use TypeScript with React is essential for building production-ready applications.