
Introduction
TypeScript is a powerful superset of JavaScript that adds static type checking to your code. When combined with React, it provides enhanced developer experience, better code quality, and improved maintainability. This chapter will guide you through integrating TypeScript with React applications.
What is TypeScript?
TypeScript is a programming language developed by Microsoft that builds on JavaScript by adding static type definitions. It compiles to plain JavaScript and runs anywhere JavaScript runs.
Benefits of Using TypeScript with React
- Type Safety: Catch errors at compile time rather than runtime
- Better IDE Support: Enhanced autocomplete, refactoring, and navigation
- Self-Documenting Code: Types serve as documentation
- Improved Refactoring: Safer code changes with confidence
- Better Team Collaboration: Clear interfaces and contracts
Setting Up React with TypeScript
Creating a New React TypeScript Project
# Using Create React App with TypeScript template
npx create-react-app my-app --template typescript
# Using Vite
npm create vite@latest my-react-app -- --template react-ts
# Using Next.js with TypeScript
npx create-next-app@latest my-app --typescript
Adding TypeScript to Existing React Project
# Install TypeScript and type definitions
npm install --save-dev typescript @types/node @types/react @types/react-dom
# Create tsconfig.json
npx tsc --init
Basic tsconfig.json for React
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"es6"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}
TypeScript Basics for React
Basic Types
// Primitive types
let name: string = "John";
let age: number = 25;
let isActive: boolean = true;
let data: null = null;
let value: undefined = undefined;
// Array types
let numbers: number[] = [1, 2, 3];
let names: Array<string> = ["Alice", "Bob"];
// Object types
let user: {
name: string;
age: number;
email?: string; // Optional property
} = {
name: "John",
age: 25
};
// Union types
let id: string | number = "123";
// Literal types
let status: "loading" | "success" | "error" = "loading";
Interfaces and Types
// Interface definition
interface User {
id: number;
name: string;
email: string;
age?: number; // Optional property
readonly createdAt: Date; // Read-only property
}
// Type alias
type Theme = "light" | "dark";
type ButtonVariant = "primary" | "secondary" | "danger";
// Extending interfaces
interface Admin extends User {
permissions: string[];
}
// Generic interfaces
interface ApiResponse<T> {
data: T;
status: number;
message:
Typing React Components
Function Components
import React from 'react';
// Basic function component
const Greeting: React.FC = () => {
return <h1>Hello, World!</h1>;
};
// Function component with props
interface GreetingProps {
name: string;
age?: number;
}
const PersonalGreeting: React.FC<GreetingProps> = ({ name, age }) => {
return (
<div>
<h1>Hello, {name}!</h1>
{age && <p>You are {age} years old.</p>}
</div>
);
};
// Alternative syntax (recommended)
const PersonalGreetingAlt = ({ name, age }: GreetingProps) => {
return (
<div>
<h1>Hello, {name}!</h1>
{age && <p>You are {age} years old.</p>}
</div>
);
};
Props with Children
interface CardProps {
title: string;
children: React.ReactNode;
}
const Card: React.FC<CardProps> = ({ title, children }) => {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-content">
{children}
</div>
</div>
);
};
// Usage
<Card title="My Card">
<p>This is the card content</p>
</Card>
Class Components
import React, { Component } from 'react';
interface CounterProps {
initialValue?: number;
}
interface CounterState {
count: number;
}
class Counter extends Component<CounterProps, CounterState> {
constructor(props: CounterProps) {
super(props);
this.state = {
count: props.initialValue || 0
};
}
increment = (): void => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
Typing React Hooks
useState Hook
import React, { useState } from 'react';
const UserProfile: React.FC = () => {
// TypeScript can infer the type
const [name, setName] = useState("John");
// Explicit typing
const [age, setAge] = useState<number>(25);
// Union types
const [status, setStatus] = useState<"loading" | "success" | "error">("loading");
// Complex objects
interface User {
id: number;
name: string;
email: string;
}
const [user, setUser] = useState<User | null>(null);
return (
<div>
<p>Name: {name}</p>
<p>Age: {age}</p>
<p>Status: {status}</p>
<button onClick={() => setName("Jane")}>Change Name</button>
</div>
);
};
useEffect Hook
import React, { useState, useEffect } from 'react';
interface Post {
id: number;
title: string;
body: string;
}
const PostList: React.FC = () => {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
const fetchPosts = async (): Promise<void> => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
const data: Post[] = await response.json();
setPosts(data.slice(0, 5));
} catch (error) {
console.error('Error fetching posts:', error);
} finally {
setLoading(false);
}
};
fetchPosts();
}, []);
if (loading) {
return <div>Loading...</div>;
}
return (
<div>
{posts.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.body}</p>
</div>
))}
</div>
);
};
useRef Hook
import React, { useRef, useEffect } from 'react';
const FocusInput: React.FC = () => {
const inputRef = useRef<HTMLInputElement>(null);
const countRef = useRef<number>(0);
useEffect(() => {
// Focus the input when component mounts
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
const handleClick = (): void => {
countRef.current += 1;
console.log(`Clicked ${countRef.current} times`);
};
return (
<div>
<input ref={inputRef} type="text" placeholder="This will be focused" />
<button onClick={handleClick}>Click me</button>
</div>
);
};
useContext Hook
import React, { createContext, useContext, useState } from 'react';
// Define the context type
interface ThemeContextType {
theme: "light" | "dark";
toggleTheme: () => void;
}
// Create context with default value
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// Theme provider component
const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setTheme] = useState<"light" | "dark">("light");
const toggleTheme = (): void => {
setTheme(prev => prev === "light" ? "dark" : "light");
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
// Custom hook to use theme context
const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
// Component using the context
const ThemedButton: React.FC = () => {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
style={{
backgroundColor: theme === "light" ? "#fff" : "#333",
color: theme === "light" ? "#333" : "#fff"
}}
>
Toggle Theme (Current: {theme})
</button>
);
};
Event Handling with TypeScript
Common Event Types
import React from 'react';
const EventHandlingExample: React.FC = () => {
// Button click event
const handleClick = (event: React.MouseEvent<HTMLButtonElement>): void => {
console.log('Button clicked!', event.currentTarget);
};
// Input change event
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
console.log('Input value:', event.target.value);
};
// Form submit event
const handleSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
event.preventDefault();
console.log('Form submitted!');
};
// Key press event
const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>): void => {
if (event.key === 'Enter') {
console.log('Enter key pressed!');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
onChange={handleInputChange}
onKeyPress={handleKeyPress}
placeholder="Type something..."
/>
<button type="submit" onClick={handleClick}>
Submit
</button>
</form>
);
};
Advanced TypeScript Patterns
Generic Components
import React from 'react'
// Generic list component
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={index}>{renderItem(item, index)}</li>
))}
</ul>
);
}
// Usage
const App: React.FC = () => {
const users = [
{ id: 1, name: "John", email: "john@example.com" },
{ id: 2, name: "Jane", email: "jane@example.com" }
];
const numbers = [1, 2, 3, 4, 5];
return (
<div>
<List
items={users}
renderItem={(user) => (
<span>{user.name} - {user.email}</span>
)}
/>
<List
items={numbers}
renderItem={(number) => <strong>{number * 2}</strong>}
/>
</div>
);
};
Higher-Order Components with TypeScript
import React, { Component } from 'react';
// HOC that adds loading state
interface WithLoadingProps {
loading: boolean;
}
function withLoading<P extends object>(
WrappedComponent: React.ComponentType<P>
): React.ComponentType<P & WithLoadingProps> {
return class WithLoadingComponent extends Component<P & WithLoadingProps> {
render() {
const { loading, ...otherProps } = this.props;
if (loading) {
return <div>Loading...</div>;
}
return <WrappedComponent {...(otherProps as P)} />;
}
};
}
// Component to wrap
interface UserListProps {
users: Array<{ id: number; name: string }>;
}
const UserList: React.FC<UserListProps> = ({ users }) => (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
// Enhanced component
const UserListWithLoading = withLoading(UserList);
// Usage
const App: React.FC = () => {
const users = [{ id: 1, name: "John" }, { id: 2, name: "Jane" }];
return (
<UserListWithLoading
users={users}
loading={false}
/>
);
};
Working with APIs and TypeScript
Typed API Responses
import React, { useState, useEffect } from 'react';
// Define API response types
interface User {
id: number;
name: string;
username: string;
email: string;
address: {
street: string;
city: string;
zipcode: string;
};
}
interface ApiError {
message: string;
status: number;
}
// Custom hook for API calls
const useApi = <T>(url: string) => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<ApiError | null>(null);
useEffect(() => {
const fetchData = async (): Promise<void> => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result: T = await response.json();
setData(result);
} catch (err) {
setError({
message: err instanceof Error ? err.message : 'An error occurred',
status: 500
});
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
};
// Component using the custom hook
const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
const { data: user, loading, error } = useApi<User>(
`https://jsonplaceholder.typicode.com/users/${userId}`
);
if (loading) return <div>Loading user...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h2>{user.name}</h2>
<p>Username: {user.username}</p>
<p>Email: {user.email}</p>
<address>
{user.address.street}, {user.address.city} {user.address.zipcode}
</address>
</div>
);
};
TypeScript with React Router
import React from 'react';
import { BrowserRouter as Router, Route, Routes, useParams, Link } from 'react-router-dom';
// Define route parameters
interface UserParams {
id: string;
}
const UserDetail: React.FC = () => {
const { id } = useParams<UserParams>();
return (
<div>
<h2>User ID: {id}</h2>
<Link to="/">Back to Home</Link>
</div>
);
};
const Home: React.FC = () => {
return (
<div>
<h1>Home Page</h1>
<Link to="/user/123">Go to User 123</Link>
</div>
);
};
const App: React.FC = () => {
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/user/:id" element={<UserDetail />} />
</Routes>
</Router>
);
};
Common TypeScript Patterns and Best Practices
1. Prop Types and Default Props
interface ButtonProps {
children: React.ReactNode;
variant?: "primary" | "secondary" | "danger";
size?: "small" | "medium" | "large";
disabled?: boolean;
onClick?: () => void;
}
const Button: React.FC<ButtonProps> = ({
children,
variant = "primary",
size = "medium",
disabled = false,
onClick
}) => {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
};
2. Utility Types
// Pick specific properties
interface User {
id: number;
name: string;
email: string;
age: number;
}
type UserSummary = Pick<User, "id" | "name">;
// Omit specific properties
type CreateUser = Omit<User, "id">;
// Partial (all properties optional)
type UpdateUser = Partial<User>;
// Required (all properties required)
type RequiredUser = Required<User>;
3. Discriminated Unions
interface LoadingState {
status: "loading";
}
interface SuccessState {
status: "success";
data: any;
}
interface ErrorState {
status: "error";
error: string;
}
type AsyncState = LoadingState | SuccessState | ErrorState;
const AsyncComponent: React.FC<{ state: AsyncState }> = ({ state }) => {
switch (state.status) {
case "loading":
return <div>Loading...</div>;
case "success":
return <div>Data: {JSON.stringify(state.data)}</div>;
case "error":
return <div>Error: {state.error}</div>;
default:
// TypeScript ensures this case is never reached
const exhaustiveCheck: never = state;
return exhaustiveCheck;
}
};
TypeScript Configuration for React
Strict Mode Configuration
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
Path Mapping
{
"compilerOptions": {
"baseUrl": "src",
"paths": {
"@components/*": ["components/*"],
"@utils/*": ["utils/*"],
"@types/*": ["types/*"]
}
}
}
Common TypeScript Errors and Solutions
1. Property Does Not Exist on Type
// Error: Property 'value' does not exist on type 'EventTarget'
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value); // ✅ Correct
// console.log(event.target.value); // ❌ Error if using Event instead
};
2. Type Assertion
/// Sometimes you need to tell TypeScript about types
const element = document.getElementById('my-element') as HTMLInputElement;
element.value = 'Hello'; // Now TypeScript knows it's an input element
3. Optional Chaining and Nullish Coalescing
interface User {
name: string;
profile?: {
avatar?: string;
};
}
const UserAvatar: React.FC<{ user: User }> = ({ user }) => {
// Optional chaining
const avatarUrl = user.profile?.avatar;
// Nullish coalescing
const displayAvatar = avatarUrl ?? '/default-avatar.png';
return <img src={displayAvatar} alt="User avatar" />;
};
Testing TypeScript React Components
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
interface CounterProps {
initialValue?: number;
}
const Counter: React.FC<CounterProps> = ({ initialValue = 0 }) => {
const [count, setCount] = React.useState(initialValue);
return (
<div>
<span data-testid="count">{count}</span>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
};
// Test file
describe('Counter', () => {
test('renders with initial value', () => {
render(<Counter initialValue={5} />);
expect(screen.getByTestId('count')).toHaveTextContent('5');
});
test('increments count when button is clicked', () => {
render(<Counter />);
const button = screen.getByRole('button', { name: /increment/i });
fireEvent.click(button);
expect(screen.getByTestId('count')).toHaveTextContent('1');
});
});
Migration Strategies
Gradual Migration from JavaScript
- Start with .tsx files: Rename .js files to .tsx
- Add basic types: Start with props and state
- Enable strict mode gradually: Add strict checks one by one
- Type external libraries: Add type definitions for third-party packages
Converting a JavaScript Component
// Before (JavaScript)
const UserCard = ({ user, onEdit }) => {
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
<button onClick={() => onEdit(user.id)}>Edit</button>
</div>
);
};
// After (TypeScript)
interface User {
id: number;
name: string;
email: string;
}
interface UserCardProps {
user: User;
onEdit: (userId: number) => void;
}
const UserCard: React.FC<UserCardProps> = ({ user, onEdit }) => {
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
<button onClick={() => onEdit(user.id)}>Edit</button>
</div>
);
};
Performance Considerations
React.memo with TypeScript
interface ExpensiveComponentProps {
data: string[];
processingFunction: (data: string[]) => string[];
}
const ExpensiveComponent = React.memo<ExpensiveComponentProps>(({ data, processingFunction }) => {
const processedData = React.useMemo(() => {
return processingFunction(data);
}, [data, processingFunction]);
return (
<div>
{processedData.map((item, index) => (
<div key={index}>{item}</div>
))}
</div>
);
});
Conclusion
TypeScript significantly enhances the React development experience by providing:
- Type Safety: Catch errors early in development
- Better Developer Experience: Enhanced IDE support and autocompletion
- Improved Code Quality: Self-documenting code with clear interfaces
- Easier Refactoring: Confident code changes with type checking
- Better Team Collaboration: Clear contracts between components
Key Takeaways
- Start with basic typing and gradually add more complex types
- Use interfaces for object shapes and props
- Leverage TypeScript’s utility types for common patterns
- Take advantage of type inference where possible
- Use strict mode for better type safety
- Consider gradual migration for existing projects
Next Steps
- Practice typing various React patterns
- Explore advanced TypeScript features like mapped types and conditional types
- Learn about type-only imports and exports
- Study real-world TypeScript React projects
- Set up proper linting and tooling for TypeScript projects
TypeScript and React work beautifully together, creating a powerful development environment that scales well for large applications while maintaining excellent developer experience.