TypeScript Best Practices for React Developers
Hi, I'm Debmalya Biswas, a frontend developer who's passionate about type safety and developer experience. Today, I'm sharing TypeScript best practices I use in production React applications.
Why TypeScript?
As Debmalya Biswas, SDE, I've found TypeScript invaluable for:
- Catching bugs at compile time
- Improving code documentation
- Enhancing IDE autocomplete
- Facilitating refactoring
Essential Type Patterns
Component Props Typing
// Basic props interface
interface ButtonProps {
label: string
onClick: () => void
variant?: 'primary' | 'secondary'
disabled?: boolean
}
export function Button({ label, onClick, variant = 'primary', disabled }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled}>
{label}
</button>
)
}Children Props
import type { ReactNode } from 'react'
interface ContainerProps {
children: ReactNode
className?: string
}
export function Container({ children, className }: ContainerProps) {
return <div className={className}>{children}</div>
}Event Handlers
interface FormProps {
onSubmit: (data: FormData) => void
}
function MyForm({ onSubmit }: FormProps) {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
// Handle form submission
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// Handle input change
}
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
// Handle button click
}
return <form onSubmit={handleSubmit}>{/* Form fields */}</form>
}Advanced Patterns
Generic Components
In the Debmalya Biswas portfolio, I use generic components for flexibility:
interface ListProps<T> {
items: T[]
renderItem: (item: T) => ReactNode
keyExtractor: (item: T) => string
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map(item => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
)
}
// Usage
const users = [{ id: '1', name: 'Debmalya' }]
<List
items={users}
renderItem={user => <span>{user.name}</span>}
keyExtractor={user => user.id}
/>Discriminated Unions
type LoadingState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: any }
| { status: 'error'; error: Error }
function DataDisplay({ state }: { state: LoadingState }) {
switch (state.status) {
case 'idle':
return <div>Click to load</div>
case 'loading':
return <div>Loading...</div>
case 'success':
return <div>{state.data}</div> // TypeScript knows data exists
case 'error':
return <div>Error: {state.error.message}</div>
}
}Utility Types
// Pick specific properties
type UserPreview = Pick<User, 'id' | 'name' | 'avatar'>
// Omit properties
type UserWithoutPassword = Omit<User, 'password'>
// Partial for optional updates
type UserUpdate = Partial<User>
// Required makes all properties required
type RequiredUser = Required<Partial<User>>
// Record for key-value mappings
type ErrorMessages = Record<string, string>Hooks with TypeScript
useState
// Type inference
const [count, setCount] = useState(0) // inferred as number
// Explicit typing
const [user, setUser] = useState<User | null>(null)
// With initial value
const [items, setItems] = useState<Item[]>([])useRef
// DOM refs
const inputRef = useRef<HTMLInputElement>(null)
// Mutable value refs
const timerRef = useRef<NodeJS.Timeout | null>(null)useReducer
type State = { count: number }
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'set'; value: number }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
case 'set':
return { count: action.value }
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 })Custom Hooks
As Debmalya Biswas, frontend developer, I create typed custom hooks:
function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T) => void] {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key)
return item ? JSON.parse(item) : initialValue
} catch {
return initialValue
}
})
const setValue = (value: T) => {
setStoredValue(value)
window.localStorage.setItem(key, JSON.stringify(value))
}
return [storedValue, setValue]
}API Integration
Typed Fetch Wrapper
async function fetchApi<T>(url: string): Promise<T> {
const response = await fetch(url)
if (!response.ok) {
throw new Error('Network response was not ok')
}
return response.json()
}
// Usage
const user = await fetchApi<User>('/api/user')Form Data Handling
interface LoginForm {
email: string
password: string
}
function LoginForm() {
const [formData, setFormData] = useState<LoginForm>({
email: '',
password: ''
})
const handleChange = (field: keyof LoginForm) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
setFormData(prev => ({ ...prev, [field]: e.target.value }))
}
return (
<form>
<input value={formData.email} onChange={handleChange('email')} />
<input value={formData.password} onChange={handleChange('password')} />
</form>
)
}Best Practices
1. Avoid 'any'
// Bad
function process(data: any) { }
// Good
function process(data: unknown) {
if (typeof data === 'string') {
// TypeScript knows data is string here
}
}2. Use Type Inference
// Let TypeScript infer when obvious
const name = 'Debmalya Biswas' // string inferred
// Specify types when needed
const config: AppConfig = getConfig()3. Prefer Interfaces for Objects
// Good for objects
interface User {
id: string
name: string
}
// Good for unions/primitives
type ID = string | number
type Status = 'active' | 'inactive'4. Use Const Assertions
const routes = {
home: '/',
blog: '/blog',
about: '/about'
} as const
type Route = typeof routes[keyof typeof routes]Conclusion
TypeScript significantly improves React development. These patterns power the Debmalya Biswas website and ensure type safety across all components.
Keep coding with confidence!
*Debmalya Biswas is a frontend SDE specializing in TypeScript, React, and type-safe web development. Visit the Debmalya Biswas portfolio for more insights.*