Back to blog
TypeScript Best Practices for React Development
3 min read
TypeScript Best Practices for React Development
TypeScript has become an essential tool for building robust React applications. Here are some best practices that will help you write better, more maintainable code.
1. Define Proper Component Props
Always define explicit interfaces for your component props:
interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
onClick?: () => void;
disabled?: boolean;
}
const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'md',
onClick,
disabled = false
}) => {
return (
<button
className={`btn btn-${variant} btn-${size}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
};
2. Use Generic Types for Reusable Components
Create flexible, reusable components with generics:
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>
);
}
3. Leverage Union Types for State Management
Use union types to create type-safe state machines:
type LoadingState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: any }
| { status: 'error'; error: string };
const [state, setState] = useState<LoadingState>({ status: 'idle' });
4. Create Custom Hooks with Proper Types
Type your custom hooks properly:
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(false);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
setLoading(true);
setError(null);
try {
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 };
}
5. Use Utility Types
Leverage TypeScript's built-in utility types:
interface User {
id: number;
name: string;
email: string;
password: string;
}
// For API responses (exclude sensitive data)
type PublicUser = Omit<User, 'password'>;
// For forms (make some fields optional)
type UserForm = Partial<Pick<User, 'name' | 'email'>>;
// For updates (all fields optional except id)
type UserUpdate = Partial<User> & Pick<User, 'id'>;
6. Strict Configuration
Use strict TypeScript configuration:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true
}
}
7. Type Guards for Runtime Safety
Create type guards for runtime type checking:
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'name' in obj &&
'email' in obj
);
}
Conclusion
These TypeScript best practices will help you build more robust React applications. Remember to:
- Always type your props and state
- Use generics for reusable components
- Leverage utility types
- Create type guards for runtime safety
- Keep your TypeScript configuration strict
Happy coding!