Difficulty: Intermediate
How do you use TypeScript effectively with React? Show typing patterns for components, hooks, and event handlers.
TypeScript and React work together to provide type-safe components, hooks, and event handling. Key patterns:
- Props: Use interfaces or type aliases for component props - useState: TypeScript infers from initial value, or use explicit generic for complex types - useRef: Use generic for DOM element refs (HTMLDivElement, etc.) - Events: Use React's typed event types (ChangeEvent, MouseEvent, etc.) - Children: Use React.ReactNode for the children prop - Generic components: Components that work with any data type while maintaining type safety
Avoid using React.FC - prefer plain function declarations with typed props for simpler, more predictable types.
import { ReactNode } from 'react';
// Props interface
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick: () => void;
children: ReactNode;
}
// Function component with typed props
function Button({ variant, size = 'md', disabled = false, onClick, children }: ButtonProps) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
// Props with discriminated union
type AlertProps =
| { type: 'success'; message: string }
| { type: 'error'; message: string; retry: () => void }
| { type: 'loading' };
function Alert(props: AlertProps) {
switch (props.type) {
case 'success': return <div className="alert-success">{props.message}</div>;
case 'error': return (
<div className="alert-error">
{props.message}
<button onClick={props.retry}>Retry</button>
</div>
);
case 'loading': return <div className="alert-loading">Loading...</div>;
}
}
Type props as interfaces with explicit children: ReactNode. Discriminated unions in props enable conditional rendering with type safety.
import { useState, useRef, useEffect, useCallback } from 'react';
interface User {
id: number;
name: string;
email: string;
}
function UserProfile({ userId }: { userId: number }) {
// useState with inference
const [count, setCount] = useState(0); // inferred as number
// useState with explicit type (for complex/null initial)
const [user, setUser] = useState<User | null>(null);
const [errors, setErrors] = useState<string[]>([]);
// useRef for DOM elements
const inputRef = useRef<HTMLInputElement>(null);
const divRef = useRef<HTMLDivElement>(null);
// useRef for mutable values (no null)
const timerRef = useRef<number>(0);
useEffect(() => {
async function loadUser() {
const res = await fetch(`/api/users/${userId}`);
const data: User = await res.json();
setUser(data);
}
loadUser();
}, [userId]);
const handleSubmit = useCallback((name: string) => {
if (inputRef.current) {
inputRef.current.focus(); // OK: narrowed from null
}
}, []);
if (!user) return <div>Loading...</div>;
return <div>{user.name} ({user.email})</div>;
}
useState infers from the initial value. Use explicit generics when the initial value is null or does not represent all possible states. useRef<Element>(null) for DOM refs.
import { ChangeEvent, FormEvent, MouseEvent } from 'react';
// Typed event handlers
function LoginForm() {
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value); // string, fully typed
};
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
// form submission 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>
);
}
// Generic component
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, i) => (
<li key={keyExtractor(item)}>{renderItem(item, i)}</li>
))}
</ul>
);
}
// Usage: T is inferred from items
<List
items={users} // User[]
renderItem={(user) => <span>{user.name}</span>} // user is User
keyExtractor={(user) => String(user.id)}
/>
Use React's typed event types for handlers. Generic components accept a type parameter that flows through props, providing type safety without manual annotations.
React Components, Props Typing, useState, useRef, Event Handlers, Generic Components