TypeScript Best Practices for React Developers
Learn essential TypeScript patterns and best practices that will make your React applications more robust, maintainable, and developer-friendly.
Topics covered:

Lazar Kapsarov
Building high-performing SaaS, e-commerce & landing pages with Next.js, React & TypeScript. Helping businesses create digital experiences that convert.

TypeScript Best Practices for React Developers
TypeScript has become an essential tool for React developers, providing type safety, better IDE support, and improved code maintainability. In this guide, we'll explore the best practices that will elevate your TypeScript React development.
Component Props Typing
Interface vs Type Aliases
When defining component props, prefer interfaces for their extensibility and better error messages:
// ✅ Good - using interface for component props
interface ButtonProps {
children: React.ReactNode
variant?: 'primary' | 'secondary' | 'destructive' | 'outline'
size?: 'sm' | 'md' | 'lg'
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void
disabled?: boolean
loading?: boolean
icon?: React.ComponentType<{ className?: string }>
className?: string
}
// ✅ Extending interfaces is clean and readable
interface IconButtonProps extends ButtonProps {
'aria-label': string // Required for icon-only buttons
icon: React.ComponentType<{ className?: string }> // Required for icon buttons
}
// ✅ Type aliases for union types and complex compositions
type Status = 'idle' | 'loading' | 'success' | 'error'
type AsyncState<T> = {
data: T | null
status: Status
error: string | null
}
// ✅ Implementation with proper prop handling
const Button: React.FC<ButtonProps> = ({
children,
variant = 'primary',
size = 'md',
loading = false,
disabled = false,
icon: Icon,
onClick,
className,
...rest
}) => {
const baseClasses = 'inline-flex items-center justify-center font-medium transition-colors'
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
destructive: 'bg-red-600 text-white hover:bg-red-700',
outline: 'border border-gray-300 bg-transparent hover:bg-gray-50'
}
const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg'
}
return (
<button
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className || ''}`}
disabled={disabled || loading}
onClick={onClick}
{...rest}
>
{loading ? (
<LoadingSpinner className="mr-2" />
) : Icon ? (
<Icon className="mr-2 h-4 w-4" />
) : null}
{children}
</button>
)
}
Advanced Props Patterns
// ✅ Conditional props with discriminated unions
type AlertProps =
| {
variant: 'success'
title: string
message: string
onClose?: () => void
}
| {
variant: 'error'
title: string
message: string
onRetry: () => void
onClose?: () => void
}
| {
variant: 'loading'
title: string
message?: string
progress?: number
}
// ✅ Polymorphic components for maximum flexibility
type PolymorphicProps<T extends React.ElementType> = {
as?: T
children: React.ReactNode
className?: string
} & React.ComponentPropsWithoutRef<T>
function Text<T extends React.ElementType = 'p'>({
as,
children,
className,
...rest
}: PolymorphicProps<T>) {
const Component = as || 'p'
return (
<Component className={className} {...rest}>
{children}
</Component>
)
}
// Usage examples:
// <Text>Default paragraph</Text>
// <Text as="h1">Heading text</Text>
// <Text as="span" onClick={handleClick}>Clickable span</Text>
Generic Components
Create reusable, type-safe components with generics:
// ✅ Advanced generic select with comprehensive typing
interface SelectOption {
id: string | number
label: string
disabled?: boolean
}
interface SelectProps<T extends SelectOption> {
options: T[]
value: T | null
onChange: (value: T | null) => void
placeholder?: string
multiple?: boolean
searchable?: boolean
disabled?: boolean
error?: string
loading?: boolean
className?: string
'data-testid'?: string
}
function Select<T extends SelectOption>({
options,
value,
onChange,
placeholder = 'Select an option...',
multiple = false,
searchable = false,
disabled = false,
error,
loading = false,
className,
...rest
}: SelectProps<T>) {
const [searchTerm, setSearchTerm] = useState('')
const [isOpen, setIsOpen] = useState(false)
const filteredOptions = useMemo(() => {
if (!searchable || !searchTerm) return options
return options.filter(option =>
option.label.toLowerCase().includes(searchTerm.toLowerCase())
)
}, [options, searchTerm, searchable])
const handleSelect = (option: T) => {
onChange(option)
if (!multiple) setIsOpen(false)
}
return (
<div className={`relative ${className || ''}`} {...rest}>
<button
type="button"
className={`
w-full px-3 py-2 border rounded-md text-left
${error ? 'border-red-500' : 'border-gray-300'}
${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}
`}
onClick={() => !disabled && setIsOpen(!isOpen)}
disabled={disabled}
>
{loading ? (
<span className="flex items-center">
<LoadingSpinner size="sm" className="mr-2" />
Loading...
</span>
) : value ? (
value.label
) : (
<span className="text-gray-500">{placeholder}</span>
)}
</button>
{isOpen && !disabled && (
<div className="absolute z-50 w-full mt-1 bg-white border border-gray-300 rounded-md shadow-lg">
{searchable && (
<div className="p-2 border-b">
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
className="w-full px-2 py-1 border rounded text-sm"
autoFocus
/>
</div>
)}
<div className="max-h-60 overflow-y-auto">
{filteredOptions.map((option) => (
<button
key={option.id}
type="button"
className={`
w-full px-3 py-2 text-left hover:bg-gray-100 transition-colors
${option.disabled ? 'opacity-50 cursor-not-allowed' : ''}
${value?.id === option.id ? 'bg-blue-50 text-blue-700' : ''}
`}
onClick={() => !option.disabled && handleSelect(option)}
disabled={option.disabled}
>
{option.label}
</button>
))}
</div>
</div>
)}
{error && <p className="mt-1 text-sm text-red-600">{error}</p>}
</div>
)
}
// ✅ Usage with specific types
interface User extends SelectOption {
id: number
label: string
email: string
role: 'admin' | 'user'
}
function UserSelector() {
const [selectedUser, setSelectedUser] = useState<User | null>(null)
const users: User[] = [
{ id: 1, label: 'John Doe', email: 'john@example.com', role: 'admin' },
{ id: 2, label: 'Jane Smith', email: 'jane@example.com', role: 'user' },
]
return (
<Select<User>
options={users}
value={selectedUser}
onChange={setSelectedUser}
searchable
placeholder="Select a user..."
data-testid="user-selector"
/>
)
}
// ✅ Generic data table component
interface DataTableColumn<T> {
key: keyof T
title: string
render?: (value: T[keyof T], row: T) => React.ReactNode
sortable?: boolean
width?: string
}
interface DataTableProps<T extends Record<string, any>> {
data: T[]
columns: DataTableColumn<T>[]
loading?: boolean
emptyMessage?: string
onRowClick?: (row: T) => void
keyExtractor: (row: T) => string | number
}
function DataTable<T extends Record<string, any>>({
data,
columns,
loading = false,
emptyMessage = 'No data available',
onRowClick,
keyExtractor
}: DataTableProps<T>) {
const [sortConfig, setSortConfig] = useState<{
key: keyof T
direction: 'asc' | 'desc'
} | null>(null)
const sortedData = useMemo(() => {
if (!sortConfig) return data
return [...data].sort((a, b) => {
const aValue = a[sortConfig.key]
const bValue = b[sortConfig.key]
if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1
if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1
return 0
})
}, [data, sortConfig])
const handleSort = (key: keyof T) => {
setSortConfig(current => ({
key,
direction: current?.key === key && current.direction === 'asc' ? 'desc' : 'asc'
}))
}
if (loading) {
return <div className="p-4 text-center">Loading...</div>
}
if (data.length === 0) {
return <div className="p-4 text-center text-gray-500">{emptyMessage}</div>
}
return (
<div className="overflow-x-auto">
<table className="w-full border-collapse border border-gray-300">
<thead className="bg-gray-50">
<tr>
{columns.map((column) => (
<th
key={String(column.key)}
className={`
px-4 py-2 text-left border-b font-medium
${column.sortable ? 'cursor-pointer hover:bg-gray-100' : ''}
`}
style={{ width: column.width }}
onClick={() => column.sortable && handleSort(column.key)}
>
<div className="flex items-center gap-2">
{column.title}
{column.sortable && sortConfig?.key === column.key && (
<span>{sortConfig.direction === 'asc' ? '↑' : '↓'}</span>
)}
</div>
</th>
))}
</tr>
</thead>
<tbody>
{sortedData.map((row) => (
<tr
key={keyExtractor(row)}
className={onRowClick ? 'cursor-pointer hover:bg-gray-50' : ''}
onClick={() => onRowClick?.(row)}
>
{columns.map((column) => (
<td key={String(column.key)} className="px-4 py-2 border-b">
{column.render
? column.render(row[column.key], row)
: String(row[column.key])
}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
}
Advanced State Management Patterns
Typed useState with Complex States
Proper typing with useState for different scenarios:
// ✅ Excellent - explicit typing for nullable states
const [user, setUser] = useState<User | null>(null);
// ✅ Good - TypeScript can infer primitive types
const [count, setCount] = useState(0);
const [isVisible, setIsVisible] = useState(false);
// ✅ Advanced - discriminated union for async states
type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
const [apiState, setApiState] = useState<AsyncState<User[]>>({
status: "idle",
});
// ✅ Complex form state with validation
interface FormState {
values: {
email: string;
password: string;
confirmPassword: string;
};
errors: Partial<Record<keyof FormState["values"], string>>;
touched: Partial<Record<keyof FormState["values"], boolean>>;
isSubmitting: boolean;
}
const [formState, setFormState] = useState<FormState>({
values: { email: "", password: "", confirmPassword: "" },
errors: {},
touched: {},
isSubmitting: false,
});
// ✅ Helper functions for form state updates
const updateFormValue = <K extends keyof FormState["values"]>(
field: K,
value: FormState["values"][K],
) => {
setFormState((prev) => ({
...prev,
values: { ...prev.values, [field]: value },
errors: { ...prev.errors, [field]: undefined }, // Clear error on change
touched: { ...prev.touched, [field]: true },
}));
};
Advanced useReducer Patterns
Type-safe reducers with sophisticated action patterns:
// ✅ Comprehensive action types with payloads
type UserAction =
| { type: "FETCH_USER_REQUEST" }
| { type: "FETCH_USER_SUCCESS"; payload: { user: User; timestamp: number } }
| { type: "FETCH_USER_FAILURE"; payload: { error: string; code?: number } }
| {
type: "UPDATE_USER_FIELD";
payload: { field: keyof User; value: User[keyof User] };
}
| { type: "CLEAR_ERROR" }
| { type: "RESET_STATE" };
interface UserState {
user: User | null;
loading: boolean;
error: { message: string; code?: number } | null;
lastUpdated: number | null;
isDirty: boolean;
}
const initialUserState: UserState = {
user: null,
loading: false,
error: null,
lastUpdated: null,
isDirty: false,
};
function userReducer(state: UserState, action: UserAction): UserState {
switch (action.type) {
case "FETCH_USER_REQUEST":
return {
...state,
loading: true,
error: null,
};
case "FETCH_USER_SUCCESS":
return {
...state,
user: action.payload.user,
loading: false,
error: null,
lastUpdated: action.payload.timestamp,
isDirty: false,
};
case "FETCH_USER_FAILURE":
return {
...state,
loading: false,
error: {
message: action.payload.error,
code: action.payload.code,
},
};
case "UPDATE_USER_FIELD":
if (!state.user) return state;
return {
...state,
user: {
...state.user,
[action.payload.field]: action.payload.value,
},
isDirty: true,
error: null,
};
case "CLEAR_ERROR":
return {
...state,
error: null,
};
case "RESET_STATE":
return initialUserState;
default:
// Exhaustive check - TypeScript will error if we miss an action type
const _exhaustiveCheck: never = action;
return state;
}
}
// ✅ Custom hook with typed dispatch and actions
function useUserState(initialUser?: User) {
const [state, dispatch] = useReducer(userReducer, {
...initialUserState,
user: initialUser || null,
});
// Typed action creators
const actions = useMemo(
() => ({
fetchUser: () => dispatch({ type: "FETCH_USER_REQUEST" }),
setUser: (user: User, timestamp = Date.now()) =>
dispatch({ type: "FETCH_USER_SUCCESS", payload: { user, timestamp } }),
setError: (error: string, code?: number) =>
dispatch({ type: "FETCH_USER_FAILURE", payload: { error, code } }),
updateUserField: <K extends keyof User>(field: K, value: User[K]) =>
dispatch({ type: "UPDATE_USER_FIELD", payload: { field, value } }),
clearError: () => dispatch({ type: "CLEAR_ERROR" }),
reset: () => dispatch({ type: "RESET_STATE" }),
}),
[],
);
return { state, actions };
}
Context API with TypeScript
Type-safe context for complex application state:
// ✅ Theme context with comprehensive typing
interface ThemeContextValue {
theme: 'light' | 'dark' | 'system';
effectiveTheme: 'light' | 'dark';
setTheme: (theme: 'light' | 'dark' | 'system') => void;
colors: {
primary: string;
secondary: string;
background: string;
foreground: string;
};
}
const ThemeContext = createContext<ThemeContextValue | null>(null);
// ✅ Custom hook with proper error handling
export function useTheme(): ThemeContextValue {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
// ✅ Provider component with comprehensive logic
interface ThemeProviderProps {
children: React.ReactNode;
defaultTheme?: 'light' | 'dark' | 'system';
storageKey?: string;
}
export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'theme'
}: ThemeProviderProps) {
const [theme, setTheme] = useState<'light' | 'dark' | 'system'>(defaultTheme);
const [systemTheme, setSystemTheme] = useState<'light' | 'dark'>('light');
// System theme detection
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
setSystemTheme(mediaQuery.matches ? 'dark' : 'light');
const handleChange = (e: MediaQueryListEvent) => {
setSystemTheme(e.matches ? 'dark' : 'light');
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);
// Persistence
useEffect(() => {
const stored = localStorage.getItem(storageKey);
if (stored && ['light', 'dark', 'system'].includes(stored)) {
setTheme(stored as 'light' | 'dark' | 'system');
}
}, [storageKey]);
const effectiveTheme = theme === 'system' ? systemTheme : theme;
const colors = useMemo(() => ({
primary: effectiveTheme === 'dark' ? '#3b82f6' : '#1d4ed8',
secondary: effectiveTheme === 'dark' ? '#64748b' : '#475569',
background: effectiveTheme === 'dark' ? '#0f172a' : '#ffffff',
foreground: effectiveTheme === 'dark' ? '#f1f5f9' : '#0f172a',
}), [effectiveTheme]);
const contextValue = useMemo<ThemeContextValue>(() => ({
theme,
effectiveTheme,
setTheme: (newTheme) => {
setTheme(newTheme);
localStorage.setItem(storageKey, newTheme);
},
colors,
}), [theme, effectiveTheme, colors, storageKey]);
return (
<ThemeContext.Provider value={contextValue}>
{children}
</ThemeContext.Provider>
);
}
Advanced Custom Hooks Typing
Generic Utility Hooks
Create powerful, reusable custom hooks with proper typing:
// ✅ Enhanced localStorage hook with serialization options
interface UseLocalStorageOptions<T> {
serializer?: {
read: (value: string) => T;
write: (value: T) => string;
};
initializeWithValue?: boolean;
}
type SetValue<T> = T | ((prevValue: T) => T);
function useLocalStorage<T>(
key: string,
initialValue: T,
options: UseLocalStorageOptions<T> = {},
): [T, (value: SetValue<T>) => void, () => void] {
const {
serializer = {
read: JSON.parse,
write: JSON.stringify,
},
initializeWithValue = true,
} = options;
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === "undefined") return initialValue;
try {
const item = localStorage.getItem(key);
if (item === null) {
if (initializeWithValue) {
localStorage.setItem(key, serializer.write(initialValue));
}
return initialValue;
}
return serializer.read(item);
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
const setValue = useCallback(
(value: SetValue<T>) => {
if (typeof window === "undefined") {
console.warn(`Tried to set localStorage key "${key}" on server`);
return;
}
try {
const newValue = value instanceof Function ? value(storedValue) : value;
localStorage.setItem(key, serializer.write(newValue));
setStoredValue(newValue);
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
},
[key, storedValue, serializer],
);
const removeValue = useCallback(() => {
if (typeof window === "undefined") return;
try {
localStorage.removeItem(key);
setStoredValue(initialValue);
} catch (error) {
console.error(`Error removing localStorage key "${key}":`, error);
}
}, [key, initialValue]);
return [storedValue, setValue, removeValue];
}
// ✅ Advanced debounce hook with proper typing
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// ✅ Previous value hook with generic typing
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
Advanced API Hooks
Sophisticated data fetching patterns with comprehensive typing:
// ✅ Comprehensive API state management
interface ApiState<T> {
data: T | null;
loading: boolean;
error: ApiError | null;
lastFetch: number | null;
}
interface ApiError {
message: string;
status?: number;
code?: string;
}
interface UseApiOptions {
enabled?: boolean;
refetchInterval?: number;
retry?: number;
retryDelay?: number;
staleTime?: number;
}
interface UseApiResult<T> extends ApiState<T> {
refetch: () => Promise<T | null>;
mutate: (data: T | null) => void;
isStale: boolean;
}
function useApi<T>(url: string, options: UseApiOptions = {}): UseApiResult<T> {
const {
enabled = true,
refetchInterval,
retry = 0,
retryDelay = 1000,
staleTime = 5 * 60 * 1000, // 5 minutes
} = options;
const [state, setState] = useState<ApiState<T>>({
data: null,
loading: enabled,
error: null,
lastFetch: null,
});
const retryCountRef = useRef(0);
const abortControllerRef = useRef<AbortController | null>(null);
const fetchData = useCallback(async (): Promise<T | null> => {
if (!enabled) return null;
// Cancel previous request
abortControllerRef.current?.abort();
abortControllerRef.current = new AbortController();
setState((prev) => ({ ...prev, loading: true, error: null }));
try {
const response = await fetch(url, {
signal: abortControllerRef.current.signal,
});
if (!response.ok) {
throw new ApiError({
message: `HTTP ${response.status}: ${response.statusText}`,
status: response.status,
});
}
const data: T = await response.json();
const now = Date.now();
setState({
data,
loading: false,
error: null,
lastFetch: now,
});
retryCountRef.current = 0;
return data;
} catch (error) {
if (error.name === "AbortError") return null;
const apiError: ApiError = {
message: error instanceof Error ? error.message : "Unknown error",
status: error.status,
code: error.code,
};
if (retryCountRef.current < retry) {
retryCountRef.current++;
setTimeout(() => fetchData(), retryDelay * retryCountRef.current);
return null;
}
setState((prev) => ({
...prev,
loading: false,
error: apiError,
}));
return null;
}
}, [url, enabled, retry, retryDelay]);
// Initial fetch
useEffect(() => {
if (enabled) {
fetchData();
}
return () => {
abortControllerRef.current?.abort();
};
}, [fetchData]);
// Refetch interval
useEffect(() => {
if (!refetchInterval || !enabled) return;
const interval = setInterval(fetchData, refetchInterval);
return () => clearInterval(interval);
}, [fetchData, refetchInterval, enabled]);
const mutate = useCallback((data: T | null) => {
setState((prev) => ({
...prev,
data,
lastFetch: data ? Date.now() : prev.lastFetch,
}));
}, []);
const isStale = useMemo(() => {
if (!state.lastFetch) return true;
return Date.now() - state.lastFetch > staleTime;
}, [state.lastFetch, staleTime]);
return {
...state,
refetch: fetchData,
mutate,
isStale,
};
}
// ✅ Mutation hook for POST/PUT/DELETE operations
interface UseMutationOptions<TVariables, TData> {
onSuccess?: (data: TData, variables: TVariables) => void;
onError?: (error: ApiError, variables: TVariables) => void;
onSettled?: (
data: TData | null,
error: ApiError | null,
variables: TVariables,
) => void;
}
interface MutationState<TData> {
data: TData | null;
loading: boolean;
error: ApiError | null;
}
interface MutationResult<TVariables, TData> extends MutationState<TData> {
mutate: (variables: TVariables) => Promise<TData | null>;
reset: () => void;
}
function useMutation<TVariables, TData>(
mutationFn: (variables: TVariables) => Promise<TData>,
options: UseMutationOptions<TVariables, TData> = {},
): MutationResult<TVariables, TData> {
const { onSuccess, onError, onSettled } = options;
const [state, setState] = useState<MutationState<TData>>({
data: null,
loading: false,
error: null,
});
const mutate = useCallback(
async (variables: TVariables): Promise<TData | null> => {
setState({ data: null, loading: true, error: null });
try {
const data = await mutationFn(variables);
setState({ data, loading: false, error: null });
onSuccess?.(data, variables);
onSettled?.(data, null, variables);
return data;
} catch (error) {
const apiError: ApiError = {
message: error instanceof Error ? error.message : "Mutation failed",
status: error.status,
code: error.code,
};
setState({ data: null, loading: false, error: apiError });
onError?.(apiError, variables);
onSettled?.(null, apiError, variables);
return null;
}
},
[mutationFn, onSuccess, onError, onSettled],
);
const reset = useCallback(() => {
setState({ data: null, loading: false, error: null });
}, []);
return { ...state, mutate, reset };
}
Advanced Event Handling Patterns
Comprehensive Event Handler Typing
Master TypeScript event handling with sophisticated patterns:
// ✅ Generic event handler types for reusability
type EventHandler<T extends HTMLElement, E extends Event = Event> = (
event: E & { currentTarget: T }
) => void;
type ChangeHandler<T extends HTMLElement> = EventHandler<
T,
React.ChangeEvent<T>
>;
type ClickHandler<T extends HTMLElement> = EventHandler<
T,
React.MouseEvent<T>
>;
type KeyHandler<T extends HTMLElement> = EventHandler<
T,
React.KeyboardEvent<T>
>;
// ✅ Advanced form component with comprehensive event handling
interface FormData {
email: string;
password: string;
rememberMe: boolean;
preferences: {
newsletter: boolean;
notifications: 'all' | 'important' | 'none';
};
}
interface FormValidation {
isValid: boolean;
errors: Partial<Record<keyof FormData, string>>;
}
interface AdvancedFormProps {
initialData?: Partial<FormData>;
onSubmit: (data: FormData) => Promise<void>;
onValidation?: (validation: FormValidation) => void;
onFieldChange?: <K extends keyof FormData>(
field: K,
value: FormData[K],
isValid: boolean
) => void;
}
const AdvancedForm: React.FC<AdvancedFormProps> = ({
initialData = {},
onSubmit,
onValidation,
onFieldChange,
}) => {
const [formData, setFormData] = useState<FormData>({
email: '',
password: '',
rememberMe: false,
preferences: {
newsletter: true,
notifications: 'important',
},
...initialData,
});
const [errors, setErrors] = useState<Partial<Record<keyof FormData, string>>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
// ✅ Strongly typed field update handler
const handleFieldChange = useCallback(
<K extends keyof FormData>(field: K, value: FormData[K]) => {
setFormData(prev => ({ ...prev, [field]: value }));
// Clear error for this field
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
}
// Validate field and notify parent
const isValid = validateField(field, value);
onFieldChange?.(field, value, isValid);
},
[errors, onFieldChange]
);
// ✅ Email input with proper typing
const handleEmailChange: ChangeHandler<HTMLInputElement> = (e) => {
const email = e.currentTarget.value;
handleFieldChange('email', email);
};
// ✅ Password input with visibility toggle
const [showPassword, setShowPassword] = useState(false);
const handlePasswordChange: ChangeHandler<HTMLInputElement> = (e) => {
handleFieldChange('password', e.currentTarget.value);
};
const togglePasswordVisibility: ClickHandler<HTMLButtonElement> = (e) => {
e.preventDefault();
setShowPassword(prev => !prev);
};
// ✅ Checkbox handling
const handleCheckboxChange: ChangeHandler<HTMLInputElement> = (e) => {
const { name, checked } = e.currentTarget;
if (name === 'rememberMe') {
handleFieldChange('rememberMe', checked);
} else if (name === 'newsletter') {
handleFieldChange('preferences', {
...formData.preferences,
newsletter: checked,
});
}
};
// ✅ Select/dropdown handling
const handleNotificationChange: ChangeHandler<HTMLSelectElement> = (e) => {
const value = e.currentTarget.value as FormData['preferences']['notifications'];
handleFieldChange('preferences', {
...formData.preferences,
notifications: value,
});
};
// ✅ Form submission with error handling
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const validation = validateForm(formData);
setErrors(validation.errors);
onValidation?.(validation);
if (!validation.isValid) return;
setIsSubmitting(true);
try {
await onSubmit(formData);
} catch (error) {
console.error('Form submission error:', error);
} finally {
setIsSubmitting(false);
}
};
// ✅ Keyboard shortcuts
const handleKeyDown: KeyHandler<HTMLFormElement> = (e) => {
// Submit on Ctrl/Cmd + Enter
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
handleSubmit(e as any);
}
};
return (
<form onSubmit={handleSubmit} onKeyDown={handleKeyDown} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email
</label>
<input
id="email"
type="email"
value={formData.email}
onChange={handleEmailChange}
className={`mt-1 block w-full rounded-md border ${
errors.email ? 'border-red-500' : 'border-gray-300'
}`}
required
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
<p id="email-error" className="mt-1 text-sm text-red-600">
{errors.email}
</p>
)}
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium">
Password
</label>
<div className="relative">
<input
id="password"
type={showPassword ? 'text' : 'password'}
value={formData.password}
onChange={handlePasswordChange}
className={`mt-1 block w-full rounded-md border pr-10 ${
errors.password ? 'border-red-500' : 'border-gray-300'
}`}
required
/>
<button
type="button"
onClick={togglePasswordVisibility}
className="absolute inset-y-0 right-0 flex items-center pr-3"
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? '🙈' : '👁️'}
</button>
</div>
{errors.password && (
<p className="mt-1 text-sm text-red-600">{errors.password}</p>
)}
</div>
<div className="space-y-2">
<label className="flex items-center">
<input
type="checkbox"
name="rememberMe"
checked={formData.rememberMe}
onChange={handleCheckboxChange}
className="mr-2"
/>
Remember me
</label>
<label className="flex items-center">
<input
type="checkbox"
name="newsletter"
checked={formData.preferences.newsletter}
onChange={handleCheckboxChange}
className="mr-2"
/>
Subscribe to newsletter
</label>
</div>
<div>
<label htmlFor="notifications" className="block text-sm font-medium">
Notification Preferences
</label>
<select
id="notifications"
value={formData.preferences.notifications}
onChange={handleNotificationChange}
className="mt-1 block w-full rounded-md border border-gray-300"
>
<option value="all">All notifications</option>
<option value="important">Important only</option>
<option value="none">None</option>
</select>
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full rounded-md bg-blue-600 py-2 px-4 text-white disabled:opacity-50"
>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
};
// ✅ Validation helper functions
function validateField<K extends keyof FormData>(
field: K,
value: FormData[K]
): boolean {
switch (field) {
case 'email':
return typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
case 'password':
return typeof value === 'string' && value.length >= 8;
default:
return true;
}
}
function validateForm(data: FormData): FormValidation {
const errors: Partial<Record<keyof FormData, string>> = {};
if (!validateField('email', data.email)) {
errors.email = 'Please enter a valid email address';
}
if (!validateField('password', data.password)) {
errors.password = 'Password must be at least 8 characters long';
}
return {
isValid: Object.keys(errors).length === 0,
errors,
};
}
Advanced TypeScript Patterns
Sophisticated Discriminated Unions
Master complex component variants with advanced discriminated unions:
// ✅ Advanced notification system with conditional props
type BaseNotificationProps = {
id: string;
title: string;
timestamp: number;
onDismiss?: (id: string) => void;
};
type NotificationProps =
| (BaseNotificationProps & {
type: 'info';
message: string;
icon?: React.ReactNode;
})
| (BaseNotificationProps & {
type: 'success';
message: string;
duration?: number;
showProgress?: boolean;
})
| (BaseNotificationProps & {
type: 'warning';
message: string;
actions?: Array<{ label: string; onClick: () => void }>;
})
| (BaseNotificationProps & {
type: 'error';
message: string;
error?: Error;
onRetry: () => void;
maxRetries?: number;
currentRetry?: number;
})
| (BaseNotificationProps & {
type: 'loading';
message?: string;
progress?: number;
onCancel?: () => void;
});
const Notification: React.FC<NotificationProps> = (props) => {
const baseClasses = "p-4 rounded-lg shadow-lg border-l-4";
switch (props.type) {
case 'info':
return (
<div className={`${baseClasses} border-blue-500 bg-blue-50`}>
<div className="flex items-start">
{props.icon && <div className="mr-3">{props.icon}</div>}
<div>
<h4 className="font-medium text-blue-800">{props.title}</h4>
<p className="text-blue-700">{props.message}</p>
</div>
</div>
</div>
);
case 'success':
return (
<div className={`${baseClasses} border-green-500 bg-green-50 relative`}>
<div className="flex items-start justify-between">
<div>
<h4 className="font-medium text-green-800">{props.title}</h4>
<p className="text-green-700">{props.message}</p>
</div>
{props.onDismiss && (
<button
onClick={() => props.onDismiss!(props.id)}
className="text-green-500 hover:text-green-700"
>
×
</button>
)}
</div>
{props.showProgress && props.duration && (
<div className="absolute bottom-0 left-0 h-1 bg-green-500 animate-pulse" />
)}
</div>
);
case 'warning':
return (
<div className={`${baseClasses} border-yellow-500 bg-yellow-50`}>
<div>
<h4 className="font-medium text-yellow-800">{props.title}</h4>
<p className="text-yellow-700 mb-3">{props.message}</p>
{props.actions && (
<div className="flex space-x-2">
{props.actions.map((action, index) => (
<button
key={index}
onClick={action.onClick}
className="px-3 py-1 bg-yellow-200 text-yellow-800 rounded text-sm hover:bg-yellow-300"
>
{action.label}
</button>
))}
</div>
)}
</div>
</div>
);
case 'error':
return (
<div className={`${baseClasses} border-red-500 bg-red-50`}>
<div>
<h4 className="font-medium text-red-800">{props.title}</h4>
<p className="text-red-700 mb-3">{props.message}</p>
{props.error && (
<details className="mb-3">
<summary className="text-sm text-red-600 cursor-pointer">
Error Details
</summary>
<pre className="text-xs bg-red-100 p-2 rounded mt-1 overflow-x-auto">
{props.error.stack || props.error.message}
</pre>
</details>
)}
<div className="flex items-center space-x-2">
<button
onClick={props.onRetry}
className="px-3 py-1 bg-red-200 text-red-800 rounded text-sm hover:bg-red-300"
>
Retry {props.currentRetry ? `(${props.currentRetry}/${props.maxRetries || 3})` : ''}
</button>
</div>
</div>
</div>
);
case 'loading':
return (
<div className={`${baseClasses} border-gray-500 bg-gray-50`}>
<div className="flex items-center justify-between">
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-600 mr-3" />
<div>
<h4 className="font-medium text-gray-800">{props.title}</h4>
{props.message && <p className="text-gray-600">{props.message}</p>}
</div>
</div>
{props.onCancel && (
<button
onClick={props.onCancel}
className="text-gray-500 hover:text-gray-700"
>
Cancel
</button>
)}
</div>
{typeof props.progress === 'number' && (
<div className="mt-3">
<div className="flex justify-between text-xs text-gray-600">
<span>Progress</span>
<span>{Math.round(props.progress * 100)}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${props.progress * 100}%` }}
/>
</div>
</div>
)}
</div>
);
default:
// TypeScript ensures this is never reached
const _exhaustiveCheck: never = props;
return null;
}
};
Advanced Utility Types and Transformations
Leverage sophisticated TypeScript utility patterns:
// ✅ Comprehensive user interface
interface User {
id: string;
name: string;
email: string;
avatar?: string;
profile: {
bio?: string;
location?: string;
website?: string;
};
preferences: {
theme: "light" | "dark" | "auto";
notifications: {
email: boolean;
push: boolean;
sms: boolean;
};
};
metadata: {
createdAt: Date;
updatedAt: Date;
lastLoginAt?: Date;
};
}
// ✅ Advanced utility type combinations
type CreateUserInput = Omit<User, "id" | "metadata"> & {
metadata?: Pick<User["metadata"], "createdAt">;
};
type UpdateUserInput = Partial<Omit<User, "id" | "metadata">> & {
id: string;
};
type UserPublicProfile = Pick<User, "id" | "name" | "avatar" | "profile"> & {
isOnline?: boolean;
};
type UserPreferences = User["preferences"];
type NotificationSettings = UserPreferences["notifications"];
// ✅ Conditional types for dynamic API responses
type ApiResponse<T> =
| {
success: true;
data: T;
meta?: {
pagination?: {
page: number;
limit: number;
total: number;
pages: number;
};
};
}
| {
success: false;
error: {
message: string;
code: string;
details?: Record<string, any>;
};
};
// ✅ Mapped types for form field validation
type ValidationResult<T> = {
[K in keyof T]?: {
isValid: boolean;
message?: string;
};
};
type FormErrors<T> = {
[K in keyof T]?: string;
};
// ✅ Deep partial for nested updates
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// ✅ Template literal types for API endpoints
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiEndpoint = `/api/${"users" | "posts" | "comments"}${"" | `/${string}`}`;
// ✅ Branded types for type safety
type UserId = string & { readonly brand: unique symbol };
type Email = string & { readonly brand: unique symbol };
function createUserId(id: string): UserId {
if (!id || typeof id !== "string") {
throw new Error("Invalid user ID");
}
return id as UserId;
}
function createEmail(email: string): Email {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new Error("Invalid email format");
}
return email as Email;
}
// ✅ Function overloads for flexible APIs
function fetchUser(id: UserId): Promise<User>;
function fetchUser(email: Email): Promise<User>;
function fetchUser(ids: UserId[]): Promise<User[]>;
function fetchUser(criteria: { name?: string; email?: Email }): Promise<User[]>;
async function fetchUser(
param: UserId | Email | UserId[] | { name?: string; email?: Email },
): Promise<User | User[]> {
if (typeof param === "string") {
// Handle single ID or email
const isEmail = param.includes("@");
const response = await fetch(
`/api/users/${isEmail ? "by-email" : "by-id"}/${param}`,
);
return response.json();
}
if (Array.isArray(param)) {
// Handle multiple IDs
const response = await fetch("/api/users/batch", {
method: "POST",
body: JSON.stringify({ ids: param }),
});
return response.json();
}
// Handle criteria object
const queryParams = new URLSearchParams();
if (param.name) queryParams.set("name", param.name);
if (param.email) queryParams.set("email", param.email);
const response = await fetch(`/api/users/search?${queryParams}`);
return response.json();
}
// ✅ Generic constraint patterns
interface Repository<T extends { id: string }> {
findById(id: string): Promise<T | null>;
findMany(criteria: Partial<T>): Promise<T[]>;
create(data: Omit<T, "id">): Promise<T>;
update(id: string, data: Partial<T>): Promise<T>;
delete(id: string): Promise<void>;
}
class UserRepository implements Repository<User> {
async findById(id: string): Promise<User | null> {
const response = await fetch(`/api/users/${id}`);
return response.ok ? response.json() : null;
}
async findMany(criteria: Partial<User>): Promise<User[]> {
const queryParams = new URLSearchParams();
Object.entries(criteria).forEach(([key, value]) => {
if (value !== undefined) queryParams.set(key, String(value));
});
const response = await fetch(`/api/users?${queryParams}`);
return response.json();
}
async create(data: Omit<User, "id">): Promise<User> {
const response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return response.json();
}
async update(id: string, data: Partial<User>): Promise<User> {
const response = await fetch(`/api/users/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return response.json();
}
async delete(id: string): Promise<void> {
await fetch(`/api/users/${id}`, { method: "DELETE" });
}
}
Best Practices Summary
Key Takeaways
- Always use explicit types - Don't rely on
any
or implicit typing - Leverage generic patterns - Create reusable, type-safe components
- Use discriminated unions - Handle complex component variants safely
- Implement proper error boundaries - Type your error handling
- Master utility types - Transform types efficiently with built-in utilities
- Create custom hooks - Encapsulate logic with proper typing
- Handle events properly - Use specific event handler types
- Validate at boundaries - Type your API interactions and form inputs
Development Workflow
- Start with interfaces - Define your data structures first
- Build incrementally - Add types as you develop features
- Test thoroughly - Use TypeScript's compiler as your first test
- Refactor confidently - Let types guide safe refactoring
- Document with types - Use TypeScript as living documentation
By following these patterns, you'll build React applications that are not only type-safe but also maintainable, scalable, and robust. TypeScript's type system becomes your ally in creating better software architecture and catching bugs before they reach production.