TypeScript Best Practices for React Developers

TypeScript Best Practices for React Developers
TypeScript has become the standard in React development. In this article, we'll explore best practices that help you write better TypeScript code that's more maintainable and has fewer bugs.
Why TypeScript Matters
TypeScript brings many benefits:
- Type safety: Catch errors at compile time instead of runtime
- Better IDE support: Excellent autocomplete and IntelliSense
- Refactoring confidence: Easy to refactor code without breaking things
- Self-documenting code: Types serve as documentation
Best Practice 1: Use Interfaces for Props
❌ Don't
type ButtonProps = {
text: string;
onClick: any; // Avoid using any!
};
✅ Do
interface ButtonProps {
text: string;
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
variant?: "primary" | "secondary" | "outline";
disabled?: boolean;
children?: React.ReactNode;
}
export function Button({ text, onClick, variant = "primary", disabled = false }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{text}
</button>
);
}
Why?
- Interfaces can be extended and merged easily
- Clear naming convention:
ComponentNameProps
- Optional properties with
?
- Default values in destructuring
Best Practice 2: Generic Components
Generic components help create reusable components with type safety.
Example: Generic List Component
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
emptyMessage?: string;
}
function List<T>({ items, renderItem, keyExtractor, emptyMessage = "No items" }: ListProps<T>) {
if (items.length === 0) {
return <p>{emptyMessage}</p>;
}
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
// Usage
interface User {
id: number;
name: string;
email: string;
}
function UserList({ users }: { users: User[] }) {
return (
<List<User>
items={users}
renderItem={(user) => (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
)}
keyExtractor={(user) => user.id}
emptyMessage="No users yet"
/>
);
}
Best Practice 3: Union Types for Props Variants
Use union types to enforce correct prop combinations.
Example: Button with Multiple Variants
type BaseButtonProps = {
children: React.ReactNode;
disabled?: boolean;
className?: string;
};
type PrimaryButtonProps = BaseButtonProps & {
variant: "primary";
onClick: () => void;
};
type LinkButtonProps = BaseButtonProps & {
variant: "link";
href: string;
target?: "_blank" | "_self";
};
type SubmitButtonProps = BaseButtonProps & {
variant: "submit";
form: string;
};
type ButtonProps = PrimaryButtonProps | LinkButtonProps | SubmitButtonProps;
function Button(props: ButtonProps) {
const { variant, children, disabled, className } = props;
if (variant === "link") {
return (
<a
href={props.href}
target={props.target}
className={className}
>
{children}
</a>
);
}
if (variant === "submit") {
return (
<button
type="submit"
form={props.form}
disabled={disabled}
className={className}
>
{children}
</button>
);
}
return (
<button
onClick={props.onClick}
disabled={disabled}
className={className}
>
{children}
</button>
);
}
TypeScript will enforce the correct props for each variant!
Best Practice 4: Utility Types
Leverage TypeScript's built-in utility types.
Partial<T>
interface User {
id: number;
name: string;
email: string;
bio: string;
}
// Update user - don't need all fields
function updateUser(id: number, updates: Partial<User>) {
// API call to update user
}
updateUser(1, { name: "New Name" }); // ✅ OK
updateUser(1, { email: "new@email.com", bio: "New bio" }); // ✅ OK
Pick<T, K>
and Omit<T, K>
// Pick only some fields
type UserPreview = Pick<User, "id" | "name">;
// Omit some fields
type UserWithoutId = Omit<User, "id">;
// Usage in components
interface UserCardProps {
user: UserPreview; // Only need id and name
}
Record<K, T>
// Map from string key to value type
type UserRole = "admin" | "editor" | "viewer";
const permissions: Record<UserRole, string[]> = {
admin: ["read", "write", "delete"],
editor: ["read", "write"],
viewer: ["read"],
};
Best Practice 5: Custom Hooks with TypeScript
Type your custom hooks properly.
import { useState, useEffect } from 'react';
interface UseFetchResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
setError(null);
} catch (e) {
setError(e instanceof Error ? e : new Error('Unknown error'));
setData(null);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [url]);
return { data, loading, error, refetch: fetchData };
}
// Usage
interface User {
id: number;
name: string;
}
function UserProfile({ userId }: { userId: number }) {
const { data: user, loading, error } = useFetch<User>(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return null;
return <div>{user.name}</div>;
}
Best Practice 6: Event Handlers
Type event handlers correctly for better type safety.
interface FormProps {
onSubmit: (data: FormData) => void;
}
function MyForm({ onSubmit }: FormProps) {
// Form submit handler
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
onSubmit(formData);
};
// Input change handler
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
// Button click handler
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log('Button clicked', e.currentTarget);
};
return (
<form onSubmit={handleSubmit}>
<input type="text" onChange={handleChange} />
<button onClick={handleClick}>Submit</button>
</form>
);
}
Best Practice 7: Avoid Type Assertions
❌ Don't
const value = JSON.parse(jsonString) as User; // Unsafe!
✅ Do
import { z } from 'zod';
// Define schema with Zod
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
function parseUser(jsonString: string): User {
const parsed = JSON.parse(jsonString);
return UserSchema.parse(parsed); // Runtime validation!
}
Best Practice 8: Discriminated Unions
Use discriminated unions for complex state management.
type RequestState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
function DataComponent() {
const [state, setState] = useState<RequestState<User>>({ status: 'idle' });
// TypeScript knows exactly what properties are available for each status
if (state.status === 'loading') {
return <div>Loading...</div>;
}
if (state.status === 'error') {
return <div>Error: {state.error}</div>; // TypeScript knows 'error' exists
}
if (state.status === 'success') {
return <div>{state.data.name}</div>; // TypeScript knows 'data' exists
}
return <button onClick={() => setState({ status: 'loading' })}>Load Data</button>;
}
Conclusion
TypeScript best practices help you:
✅ Avoid bugs: Type safety catches errors early
✅ More maintainable code: Self-documenting code
✅ Better developer experience: Autocomplete and IntelliSense
✅ Easy refactoring: Confidence when changing code
Key Takeaways
- Use interfaces for component props
- Leverage generic types for reusable components
- Use union types for variant props
- Take advantage of utility types (Partial, Pick, Omit, Record)
- Type custom hooks properly
- Type event handlers correctly
- Avoid type assertions, use validation instead
- Use discriminated unions for complex state
Next Steps
- Practice these patterns in your projects
- Explore more advanced TypeScript features
- Check out the TypeScript Handbook
Happy coding! 🚀

Cong Dinh
Technology Consultant | Trainer | Solution Architect
With over 10 years of experience in web development and cloud architecture, I help businesses build modern and sustainable technology solutions. Expertise: Next.js, TypeScript, AWS, and Solution Architecture.
Related Posts
TypeScript Best Practices 2025: Writing Clean and Safe Code
Explore modern TypeScript patterns, utility types, and best practices to write type-safe and maintainable code that helps teams develop more effectively.
Getting Started with Next.js 15: Comprehensive Guide
Discover the new features of Next.js 15 and learn how to build modern applications with App Router, Server Components, and Server Actions for optimal performance.

Tailwind CSS Tips and Tricks to Boost Productivity
Discover tips, tricks, and best practices when using Tailwind CSS to build UI faster and more maintainably in Next.js projects