TypeScript Tips for React Developers
TypeScript has become an essential tool in modern React development. It catches bugs early, improves developer experience with autocomplete, and makes refactoring safer. Let's explore some advanced TypeScript patterns that will level up your React code.
Proper Component Typing
Functional Components
The best way to type functional components has evolved:
// ✅ Recommended
type ButtonProps = {
label: string
onClick: () => void
variant?: 'primary' | 'secondary'
}
function Button({ label, onClick, variant = 'primary' }: ButtonProps) {
return (
<button onClick={onClick} className={variant}>
{label}
</button>
)
}
Props with Children
Handle children properly with React.ReactNode:
type CardProps = {
title: string
children: React.ReactNode
}
function Card({ title, children }: CardProps) {
return (
<div>
<h2>{title}</h2>
{children}
</div>
)
}
Advanced Patterns
Generic Components
Create reusable components with generics:
type SelectProps<T> = {
options: T[]
value: T
onChange: (value: T) => void
getLabel: (option: T) => string
}
function Select<T>({ options, value, onChange, getLabel }: SelectProps<T>) {
return (
<select value={String(value)} onChange={(e) => onChange(options[Number(e.target.value)])}>
{options.map((option, index) => (
<option key={index} value={index}>
{getLabel(option)}
</option>
))}
</select>
)
}
Discriminated Unions
Model complex state with discriminated unions:
type LoadingState = { status: 'loading' }
type ErrorState = { status: 'error'; error: string }
type SuccessState<T> = { status: 'success'; data: T }
type AsyncState<T> = LoadingState | ErrorState | SuccessState<T>
function DataDisplay<T>({ state }: { state: AsyncState<T> }) {
switch (state.status) {
case 'loading':
return <Spinner />
case 'error':
return <Error message={state.error} />
case 'success':
return <Data data={state.data} />
}
}
Hooks Typing
useState with Complex Types
type User = {
id: string
name: string
email: string
}
// TypeScript infers the type
const [user, setUser] = useState<User | null>(null)
Custom Hooks
type UseToggleReturn = {
isOn: boolean
toggle: () => void
setOn: () => void
setOff: () => void
}
function useToggle(initialValue = false): UseToggleReturn {
const [isOn, setIsOn] = useState(initialValue)
return {
isOn,
toggle: () => setIsOn((prev) => !prev),
setOn: () => setIsOn(true),
setOff: () => setIsOn(false),
}
}
Event Handling
Type events properly for better type safety:
function Form() {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
// Form logic
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value)
}
return (
<form onSubmit={handleSubmit}>
<input type="text" onChange={handleChange} />
<button type="submit">Submit</button>
</form>
)
}
Utility Types
Leverage TypeScript's built-in utility types:
type User = {
id: string
name: string
email: string
password: string
}
// Pick specific properties
type UserProfile = Pick<User, 'id' | 'name' | 'email'>
// Omit sensitive data
type PublicUser = Omit<User, 'password'>
// Make all properties optional
type PartialUser = Partial<User>
// Make all properties required
type RequiredUser = Required<User>
Best Practices
- Avoid
any: Useunknownwhen you're unsure of a type - Use strict mode: Enable
"strict": truein tsconfig.json - Prefer interfaces for objects: They're more extensible
- Use type for unions:
type Status = 'idle' | 'loading' | 'error' - Leverage type inference: Don't over-annotate
Conclusion
TypeScript transforms the React development experience by catching errors before runtime and providing excellent IDE support. These patterns will help you write more robust, maintainable code.
Start small, gradually introduce these patterns into your codebase, and watch your confidence in refactoring grow. The upfront investment in proper typing pays dividends in the long run.