TypeScript Best Practices for React Developers
TypeScript has become the standard for building robust React applications. In this comprehensive guide, we'll explore best practices that will make you a TypeScript pro.
Why TypeScript with React?
TypeScript adds static typing to JavaScript, catching errors at compile time rather than runtime.
💡 NOTE
Studies show that TypeScript can prevent up to 15% of bugs before they reach production!
Basic Type Annotations
Component Props
Always type your component props:
interface ButtonProps {
label : string ;
onClick : () => void ;
variant ?: 'primary' | 'secondary' ;
disabled ?: boolean ;
}
function Button ({ label , onClick , variant = 'primary' , disabled = false } : ButtonProps ) {
return (
< button
onClick = {onClick}
disabled = {disabled}
className = { `btn btn-${ variant }` }
>
{ label }
</ button >
);
}
State Types
Type your state correctly:
import { useState } from 'react' ;
interface User {
id : number ;
name : string ;
email : string ;
}
function UserProfile () {
const [ user , setUser ] = useState < User | null >( null );
const [ loading , setLoading ] = useState < boolean >( false );
return (
< div >
{ user && < p >{ user . name }</ p >}
</ div >
);
}
Advanced Patterns
Generic Components
Create reusable 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
< List
items = {users}
renderItem = {(user) => <span>{user.name}</span>}
keyExtractor = {(user) => user.id}
/>
Discriminated Unions
Handle different states elegantly:
type FetchState < T > =
| { status : 'idle' }
| { status : 'loading' }
| { status : 'success' ; data : T }
| { status : 'error' ; error : string };
function DataComponent () {
const [ state , setState ] = useState < FetchState < User >>({ status: 'idle' });
if (state.status === 'loading' ) {
return < Spinner />;
}
if (state.status === 'error' ) {
return < Error message ={ state . error } />;
}
if (state.status === 'success' ) {
return < UserCard user ={ state . data } />;
}
return < button onClick ={ fetchUser }>Load User </ button > ;
}
✅ SUCCESS
Discriminated unions provide excellent type narrowing and IDE support!
Utility Types
Leverage TypeScript's utility types:
interface User {
id : number ;
name : string ;
email : string ;
password : string ;
}
// Pick specific properties
type UserPublic = Pick < User , 'id' | 'name' | 'email' >;
// Omit sensitive data
type UserSafe = Omit < User , 'password' >;
// Make all properties optional
type UserPartial = Partial < User >;
// Make all properties required
type UserRequired = Required < UserPartial >;
// Make all properties readonly
type UserReadonly = Readonly < User >;
Custom Hooks with TypeScript
Typed Custom Hook
interface UseLocalStorageReturn < T > {
value : T ;
setValue : ( value : T ) => void ;
removeValue : () => void ;
}
function useLocalStorage < T >(
key : string ,
initialValue : T
) : UseLocalStorageReturn < T > {
const [ value , setValue ] = useState < T >(() => {
const stored = localStorage. getItem (key);
return stored ? JSON . parse (stored) : initialValue;
});
const setStoredValue = ( newValue : T ) => {
setValue (newValue);
localStorage. setItem (key, JSON . stringify (newValue));
};
const removeValue = () => {
setValue (initialValue);
localStorage. removeItem (key);
};
return { value, setValue: setStoredValue, removeValue };
}
Fetch Hook
interface UseFetchResult < T > {
data : T | null ;
loading : boolean ;
error : Error | null ;
refetch : () => Promise < 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);
const json = await response. json ();
setData (json);
} catch (err) {
setError (err as Error );
} finally {
setLoading ( false );
}
};
useEffect (() => {
fetchData ();
}, [url]);
return { data, loading, error, refetch: fetchData };
}
Event Handlers
Type event handlers correctly:
import { ChangeEvent, FormEvent, MouseEvent } from 'react' ;
function Form () {
const handleChange = ( e : ChangeEvent < HTMLInputElement >) => {
console. log (e.target.value);
};
const handleSubmit = ( e : FormEvent < HTMLFormElement >) => {
e. preventDefault ();
// Submit logic
};
const handleClick = ( e : MouseEvent < HTMLButtonElement >) => {
console. log (e.clientX, e.clientY);
};
return (
< form onSubmit = {handleSubmit} >
< input onChange = {handleChange} />
< button onClick = {handleClick} > Submit </ button >
</ form >
);
}
Context API with TypeScript
Type-Safe Context
import { createContext, useContext, ReactNode } from 'react' ;
interface AuthContextType {
user : User | null ;
login : ( email : string , password : string ) => Promise < void >;
logout : () => void ;
isAuthenticated : boolean ;
}
const AuthContext = createContext < AuthContextType | undefined >( undefined );
export function AuthProvider ({ children } : { children : ReactNode }) {
// Implementation
const value : AuthContextType = {
user,
login,
logout,
isAuthenticated: !! user,
};
return < AuthContext . Provider value ={ value }>{children} </ AuthContext.Provider > ;
}
export function useAuth () {
const context = useContext (AuthContext);
if (context === undefined ) {
throw new Error ( 'useAuth must be used within AuthProvider' );
}
return context;
}
Common Pitfalls to Avoid
1. Using any
// ❌ Bad
const data : any = fetchData ();
// ✅ Good
interface ApiResponse {
data : User [];
total : number ;
}
const data : ApiResponse = await fetchData ();
2. Not Using Strict Mode
Enable strict mode in tsconfig.json :
{
"compilerOptions" : {
"strict" : true ,
"strictNullChecks" : true ,
"strictFunctionTypes" : true ,
"noImplicitAny" : true
}
}
💡 TIP
Always enable strict mode for maximum type safety!
3. Ignoring TypeScript Errors
// ❌ Bad
// @ts-ignore
const result = riskyOperation ();
// ✅ Good
try {
const result = riskyOperation ();
} catch (error) {
handleError (error as Error );
}
Type Guards
Create custom type guards:
function isUser ( obj : unknown ) : obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'name' in obj &&
'email' in obj
);
}
function processData ( data : unknown ) {
if ( isUser (data)) {
// TypeScript knows data is User here
console. log (data.name);
}
}
Conclusion
TypeScript significantly improves the development experience and code quality in React applications. By following these best practices, you'll write more maintainable and bug-free code.
Key Takeaways
Always type your props and state
Use utility types to transform existing types
Create reusable generic components
Enable strict mode
Avoid any type
Use discriminated unions for complex states
Happy typing! 💪