TypeScript with React
TypeScript Programming
TypeScript with React
React and TypeScript are a natural combination. TypeScript adds compile-time safety to React's component model, ensuring that props are passed correctly, state is used consistently, and event handlers receive the right event types. The React ecosystem has excellent TypeScript support, with the @types/react package providing comprehensive type definitions. Modern React development with TypeScript uses function components and hooks almost exclusively, and the type system integrates seamlessly with patterns like context, reducers, and custom hooks.
Typing Function Components
Function components in React can be typed by annotating their props parameter. The recommended approach is to define a props interface and use it directly, rather than using the React.FC type which adds implicit children and other properties.
import React from "react";
// Define props interface
interface UserCardProps {
name: string;
email: string;
avatar?: string; // optional prop
role: "admin" | "editor" | "viewer";
onSelect: (id: number) => void; // callback prop
}
// Type the props parameter directly (recommended)
function UserCard({ name, email, avatar, role, onSelect }: UserCardProps) {
return (
<div className="user-card">
{avatar && <img src={avatar} alt={name} />}
<h3>{name}</h3>
<p>{email}</p>
<span className={`badge badge-${role}`}>{role}</span>
<button onClick={() => onSelect(1)}>Select</button>
</div>
);
}
// Usage — TypeScript validates all props
<UserCard
name="Alice"
email="alice@test.com"
role="admin"
onSelect={(id) => console.log(id)}
/>
Typing Hooks
React hooks are generic functions, and TypeScript infers types from initial values. You can also provide explicit type parameters when inference is insufficient.
import { useState, useEffect, useRef, useCallback } from "react";
interface User {
id: number;
name: string;
email: string;
}
function UserList() {
// Type inferred from initial value
const [search, setSearch] = useState("");
// Explicit type for complex state
const [users, setUsers] = useState<User[]>([]);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
// Ref with explicit type (null initial for DOM refs)
const inputRef = useRef<HTMLInputElement>(null);
// Typed callback
const fetchUsers = useCallback(async () => {
setLoading(true);
try {
const response = await fetch(`/api/users?q=${search}`);
const data: User[] = await response.json();
setUsers(data);
setError(null);
} catch (e) {
setError(e instanceof Error ? e.message : "Unknown error");
} finally {
setLoading(false);
}
}, [search]);
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
return (
<div>
<input ref={inputRef} value={search} onChange={(e) => setSearch(e.target.value)} />
{loading && <p>Loading...</p>}
{error && <p className="error">{error}</p>}
{users.map((user) => <p key={user.id}>{user.name}</p>)}
</div>
);
}
Typing Events
React provides typed event objects that correspond to native DOM events. TypeScript automatically infers event types in inline handlers, but explicit types are needed for extracted handler functions.
import React, { ChangeEvent, FormEvent, MouseEvent } from "react";
function LoginForm() {
const [email, setEmail] = useState("");
// Explicit event type for extracted handler
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
};
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log("Submitting:", email);
};
const handleButtonClick = (e: MouseEvent<HTMLButtonElement>) => {
console.log("Button clicked at:", e.clientX, e.clientY);
};
return (
<form onSubmit={handleSubmit}>
<input type="email" value={email} onChange={handleChange} />
<button type="submit" onClick={handleButtonClick}>Login</button>
</form>
);
}
Typing Context
React Context with TypeScript requires typing both the context value and providing a sensible default or handling the undefined case.
import React, { 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);
// Custom hook with type guard
function useAuth(): AuthContextType {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = async (email: string, password: string) => {
const response = await fetch("/api/login", {
method: "POST",
body: JSON.stringify({ email, password }),
});
const userData: User = await response.json();
setUser(userData);
};
const logout = () => setUser(null);
return (
<AuthContext.Provider value={{ user, login, logout, isAuthenticated: !!user }}>
{children}
</AuthContext.Provider>
);
}
Tip: Use theComponentPropsutility type from React to extract props from native HTML elements or existing components:type ButtonProps = React.ComponentProps<"button">. This is useful when wrapping native elements in custom components.
Key Takeaways
- Type component props with interfaces and annotate the props parameter directly.
- Provide explicit type parameters to hooks like
useState<T>when TypeScript cannot infer the type. - Use React's typed event objects (
ChangeEvent,FormEvent,MouseEvent) for event handlers. - Create typed Context with custom hooks that throw on undefined for safety.
- Use
ReactNodefor thechildrenprop type in wrapper components.