TypeScript with React: A Complete Beginner's Guide
Introduction
TypeScript has revolutionized the way developers build React applications by bringing static typing to JavaScript. This powerful combination provides better code quality, enhanced developer experience, and fewer runtime errors. Whether you're new to TypeScript or looking to integrate it into your React workflow, this comprehensive guide will walk you through everything you need to know.
What is TypeScript and Why Use it with React?
TypeScript is a superset of JavaScript that adds static type definitions. When combined with React, it provides several key benefits:
- Early Error Detection: Catch bugs during development rather than runtime - Enhanced IDE Support: Better autocomplete, refactoring, and navigation - Self-Documenting Code: Types serve as inline documentation - Improved Team Collaboration: Clear contracts between components - Easier Refactoring: Confident code changes with type safety
Setting Up TypeScript with React
Creating a New TypeScript React Project
The easiest way to start a new TypeScript React project is using Create React App:
`bash
npx create-react-app my-app --template typescript
cd my-app
npm start
`
This creates a project with TypeScript configuration out of the box, including:
- tsconfig.json for TypeScript configuration
- Type definitions for React (@types/react, @types/react-dom)
- Proper file extensions (.tsx for React components)
Adding TypeScript to an Existing React Project
If you have an existing React project, you can add TypeScript gradually:
`bash
npm install --save typescript @types/node @types/react @types/react-dom @types/jest
`
Create a tsconfig.json file:
`json
{
"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"
]
}
`
Understanding TypeScript Basics for React
Basic Types
Before diving into React-specific typing, let's review essential TypeScript types:
`typescript
// Primitive types
let name: string = "John";
let age: number = 30;
let isActive: boolean = true;
let data: null = null;
let value: undefined = undefined;
// Array types
let numbers: number[] = [1, 2, 3];
let names: Array
// Object types let user: { name: string; age: number; } = { name: "John", age: 30 };
// Union types let id: string | number = "123";
// Optional properties
let config: {
apiUrl: string;
timeout?: number; // Optional
} = {
apiUrl: "https://api.example.com"
};
`
Interfaces and Types
Interfaces and type aliases are crucial for defining complex structures:
`typescript
// Interface
interface User {
id: number;
name: string;
email: string;
isActive?: boolean;
}
// Type alias type Status = "loading" | "success" | "error";
// Extending interfaces interface AdminUser extends User { permissions: string[]; }
// Generic types
interface ApiResponse`
Typing React Components
Function Components
TypeScript provides several ways to type React function components:
`typescript
import React from 'react';
// Method 1: Explicit return type function Welcome(props: { name: string }): JSX.Element { return
Hello, {props.name}!
; }// Method 2: Using React.FC (Functional Component) const Welcome: React.FC<{ name: string }> = ({ name }) => { return
Hello, {name}!
; };// Method 3: Inline props typing (recommended) const Welcome = ({ name }: { name: string }) => { return
Hello, {name}!
; };`Class Components
For class components, extend React.Component with proper generics:
`typescript
import React, { Component } from 'react';
interface Props { title: string; count?: number; }
interface State { isVisible: boolean; }
class MyComponent extends Component
render() { const { title, count = 0 } = this.props; const { isVisible } = this.state;
return (
{title}
Count: {count}
> )}`Typing Props Effectively
Basic Props Interface
Define clear interfaces for your component props:
`typescript
interface ButtonProps {
text: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
size?: 'small' | 'medium' | 'large';
}
const Button = ({
text,
onClick,
variant = 'primary',
disabled = false,
size = 'medium'
}: ButtonProps) => {
return (
);
};
`
Children Props
Handle children props properly:
`typescript
import React, { ReactNode } from 'react';
interface CardProps { title: string; children: ReactNode; }
const Card = ({ title, children }: CardProps) => { return (
{title}
// Usage
This is the card content`
Event Handlers
Type event handlers correctly:
`typescript
import React, { ChangeEvent, FormEvent } from 'react';
interface FormProps { onSubmit: (data: { name: string; email: string }) => void; }
const ContactForm = ({ onSubmit }: FormProps) => { const [formData, setFormData] = React.useState({ name: '', email: '' });
const handleInputChange = (e: ChangeEvent
const handleSubmit = (e: FormEvent
return (
); };`Optional and Default Props
Handle optional props and defaults:
`typescript
interface ProductCardProps {
id: number;
name: string;
price: number;
description?: string;
imageUrl?: string;
onAddToCart?: (id: number) => void;
}
const ProductCard = ({ id, name, price, description = "No description available", imageUrl = "/default-product.jpg", onAddToCart }: ProductCardProps) => { return (
{name}
${price.toFixed(2)}
{description}
{onAddToCart && ( )}`Typing State Management
useState Hook
Type the useState hook properly:
`typescript
import React, { useState } from 'react';
// Simple state
const [count, setCount] = useState
// Complex state with interface interface User { id: number; name: string; email: string; }
const [user, setUser] = useState
// Array state
const [items, setItems] = useState
// State with union types
type LoadingState = 'idle' | 'loading' | 'success' | 'error';
const [status, setStatus] = useState
// Complete example
const UserProfile = () => {
const [user, setUser] = useState
const fetchUser = async (userId: number) => {
setLoading(true);
setError(null);
try {
const response = await fetch(/api/users/${userId});
const userData: User = await response.json();
setUser(userData);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
};
return (
Loading...
} {error &&Error: {error}
} {user && ({user.name}
{user.email}
`useReducer Hook
Type useReducer for complex state management:
`typescript
import React, { useReducer } from 'react';
// State interface interface TodoState { todos: Todo[]; filter: 'all' | 'active' | 'completed'; }
// Todo interface interface Todo { id: number; text: string; completed: boolean; }
// Action types type TodoAction = | { type: 'ADD_TODO'; payload: { text: string } } | { type: 'TOGGLE_TODO'; payload: { id: number } } | { type: 'DELETE_TODO'; payload: { id: number } } | { type: 'SET_FILTER'; payload: { filter: 'all' | 'active' | 'completed' } };
// Reducer function const todoReducer = (state: TodoState, action: TodoAction): TodoState => { switch (action.type) { case 'ADD_TODO': return { ...state, todos: [ ...state.todos, { id: Date.now(), text: action.payload.text, completed: false } ] }; case 'TOGGLE_TODO': return { ...state, todos: state.todos.map(todo => todo.id === action.payload.id ? { ...todo, completed: !todo.completed } : todo ) }; case 'DELETE_TODO': return { ...state, todos: state.todos.filter(todo => todo.id !== action.payload.id) }; case 'SET_FILTER': return { ...state, filter: action.payload.filter }; default: return state; } };
// Component using useReducer const TodoApp = () => { const [state, dispatch] = useReducer(todoReducer, { todos: [], filter: 'all' });
const addTodo = (text: string) => { dispatch({ type: 'ADD_TODO', payload: { text } }); };
const toggleTodo = (id: number) => { dispatch({ type: 'TOGGLE_TODO', payload: { id } }); };
return (
`Typing React Hooks
useEffect Hook
The useEffect hook doesn't require special typing, but be careful with dependencies:
`typescript
import React, { useState, useEffect } from 'react';
interface Post { id: number; title: string; body: string; }
const PostList = () => {
const [posts, setPosts] = useState
useEffect(() => { const fetchPosts = async () => { try { const response = await fetch('/api/posts'); const data: Post[] = await response.json(); setPosts(data); } catch (error) { console.error('Failed to fetch posts:', error); } finally { setLoading(false); } };
fetchPosts(); }, []); // Empty dependency array
return (
Loading...
) : ( posts.map(post => ({post.title}
{post.body}
`useRef Hook
Type useRef properly for different use cases:
`typescript
import React, { useRef, useEffect } from 'react';
const FocusInput = () => {
// For DOM elements
const inputRef = useRef
useEffect(() => { // Focus input on mount if (inputRef.current) { inputRef.current.focus(); } }, []);
const handleClick = () => { countRef.current += 1; console.log('Count:', countRef.current); };
return (
`Custom Hooks
Type custom hooks properly:
`typescript
import { useState, useEffect } from 'react';
// Custom hook for API fetching
function useApi
useEffect(() => { const fetchData = async () => { try { setLoading(true); const response = await fetch(url); if (!response.ok) { throw new Error('Network response was not ok'); } const result: T = await response.json(); setData(result); } catch (err) { setError(err instanceof Error ? err.message : 'An error occurred'); } finally { setLoading(false); } };
fetchData(); }, [url]);
return { data, loading, error }; }
// Usage interface User { id: number; name: string; email: string; }
const UserComponent = ({ userId }: { userId: number }) => {
const { data: user, loading, error } = useApi/api/users/${userId});
if (loading) return
return (
{user.name}
{user.email}
`useCallback and useMemo
Type these hooks for optimization:
`typescript
import React, { useState, useCallback, useMemo } from 'react';
interface Item { id: number; name: string; category: string; price: number; }
const ShoppingList = ({ items }: { items: Item[] }) => {
const [filter, setFilter] = useState
// useCallback for event handlers
const handleFilterChange = useCallback((e: React.ChangeEvent
const handleSortChange = useCallback((newSortBy: 'name' | 'price') => { setSortBy(newSortBy); }, []);
// useMemo for expensive calculations const filteredAndSortedItems = useMemo(() => { return items .filter(item => item.name.toLowerCase().includes(filter.toLowerCase()) || item.category.toLowerCase().includes(filter.toLowerCase()) ) .sort((a, b) => { if (sortBy === 'name') { return a.name.localeCompare(b.name); } return a.price - b.price; }); }, [items, filter, sortBy]);
return (
-
{filteredAndSortedItems.map(item => (
- {item.name} - ${item.price} ))}
`Typing Context API
Basic Context Setup
Type the Context API properly for state management:
`typescript
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
// User interface interface User { id: number; name: string; email: string; role: 'admin' | 'user'; }
// Auth state interface interface AuthState { user: User | null; isAuthenticated: boolean; loading: boolean; }
// Auth actions type AuthAction = | { type: 'LOGIN_START' } | { type: 'LOGIN_SUCCESS'; payload: User } | { type: 'LOGIN_FAILURE' } | { type: 'LOGOUT' };
// Context type
interface AuthContextType {
state: AuthState;
login: (email: string, password: string) => Promise
// Create context with default value
const AuthContext = createContext
// Auth reducer const authReducer = (state: AuthState, action: AuthAction): AuthState => { switch (action.type) { case 'LOGIN_START': return { ...state, loading: true }; case 'LOGIN_SUCCESS': return { ...state, user: action.payload, isAuthenticated: true, loading: false }; case 'LOGIN_FAILURE': return { ...state, user: null, isAuthenticated: false, loading: false }; case 'LOGOUT': return { ...state, user: null, isAuthenticated: false, loading: false }; default: return state; } };
// Provider component interface AuthProviderProps { children: ReactNode; }
export const AuthProvider = ({ children }: AuthProviderProps) => { const [state, dispatch] = useReducer(authReducer, { user: null, isAuthenticated: false, loading: false });
const login = async (email: string, password: string) => { dispatch({ type: 'LOGIN_START' }); try { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }); if (response.ok) { const user: User = await response.json(); dispatch({ type: 'LOGIN_SUCCESS', payload: user }); } else { dispatch({ type: 'LOGIN_FAILURE' }); } } catch (error) { dispatch({ type: 'LOGIN_FAILURE' }); } };
const logout = () => { dispatch({ type: 'LOGOUT' }); };
return (
// Custom hook to use auth context
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
`
Using the Typed Context
`typescript
import React from 'react';
import { useAuth } from './AuthContext';
const LoginForm = () => { const { state, login } = useAuth(); const [email, setEmail] = React.useState(''); const [password, setPassword] = React.useState('');
const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); await login(email, password); };
return (
); };const UserProfile = () => { const { state, logout } = useAuth();
if (!state.isAuthenticated || !state.user) { return
return (
Welcome, {state.user.name}!
Email: {state.user.email}
Role: {state.user.role}
`Best Practices
1. Use Strict TypeScript Configuration
Enable strict mode in your tsconfig.json:
`json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}
`
2. Prefer Interfaces for Props
Use interfaces instead of type aliases for component props:
`typescript
// Preferred
interface ButtonProps {
text: string;
onClick: () => void;
}
// Less preferred for props
type ButtonProps = {
text: string;
onClick: () => void;
};
`
3. Use Discriminated Unions for State
Use discriminated unions for complex state management:
`typescript
type LoadingState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: any }
| { status: 'error'; error: string };
const DataComponent = () => {
const [state, setState] = useState
// TypeScript will narrow the type based on status if (state.status === 'success') { // state.data is available here return
if (state.status === 'error') { // state.error is available here return
return
`4. Create Reusable Type Definitions
Create a types directory for shared interfaces:
`typescript
// types/api.ts
export interface ApiResponse
export interface PaginatedResponse
// types/user.ts export interface User { id: number; name: string; email: string; createdAt: string; }
export interface CreateUserRequest {
name: string;
email: string;
password: string;
}
`
5. Use Generic Components
Create flexible, reusable components with generics:
`typescript
interface ListProps
function List
{items.map((item, index) => (
);
}
// Usage const users: User[] = [/ user data /];
user.id}
renderItem={(user) => {user.name}}
/>
`
Project Structure for TypeScript React Projects
Recommended Folder Structure
`
src/
├── components/
│ ├── common/
│ │ ├── Button/
│ │ │ ├── Button.tsx
│ │ │ ├── Button.types.ts
│ │ │ └── index.ts
│ │ └── Modal/
│ │ ├── Modal.tsx
│ │ ├── Modal.types.ts
│ │ └── index.ts
│ └── features/
│ ├── auth/
│ │ ├── LoginForm.tsx
│ │ └── UserProfile.tsx
│ └── dashboard/
│ ├── Dashboard.tsx
│ └── DashboardCard.tsx
├── contexts/
│ ├── AuthContext.tsx
│ └── ThemeContext.tsx
├── hooks/
│ ├── useApi.ts
│ ├── useLocalStorage.ts
│ └── useDebounce.ts
├── services/
│ ├── api.ts
│ └── auth.service.ts
├── types/
│ ├── api.ts
│ ├── user.ts
│ └── index.ts
├── utils/
│ ├── formatters.ts
│ └── validators.ts
└── App.tsx
`
Component Structure Example
`typescript
// components/common/Button/Button.types.ts
export interface ButtonProps {
children: React.ReactNode;
variant?: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
loading?: boolean;
onClick?: () => void;
type?: 'button' | 'submit' | 'reset';
}
// components/common/Button/Button.tsx import React from 'react'; import { ButtonProps } from './Button.types';
const Button: React.FC
export default Button;
// components/common/Button/index.ts
export { default } from './Button';
export type { ButtonProps } from './Button.types';
`
Common TypeScript Patterns in React
Higher-Order Components (HOCs)
Type HOCs properly:
`typescript
import React, { ComponentType } from 'react';
// HOC that adds loading state interface WithLoadingProps { loading: boolean; }
function withLoading
// Usage interface UserListProps { users: User[]; }
const UserList = ({ users }: UserListProps) => (
-
{users.map(user =>
- {user.name} )}
const UserListWithLoading = withLoading(UserList);
// Usage in component
`
Render Props Pattern
`typescript
interface RenderPropsComponentProps
function DataRenderer
// Usage
{users.map(user =>
)}
/>
`
Conclusion
TypeScript with React provides a powerful development experience that catches errors early, improves code quality, and enhances team collaboration. By following the patterns and best practices outlined in this guide, you'll be well-equipped to build robust, type-safe React applications.
Key takeaways: - Start with proper project setup and configuration - Use interfaces for component props and state - Type hooks correctly for better development experience - Leverage the Context API with proper typing for state management - Follow consistent project structure and naming conventions - Use generic components for reusability - Enable strict TypeScript settings for better type safety
Remember that TypeScript is a gradual adoption tool – you can start small and add types incrementally. The investment in learning TypeScript pays dividends in code quality, maintainability, and developer productivity.
As you continue your TypeScript React journey, keep practicing these patterns and stay updated with the latest TypeScript and React features. The combination of these technologies will make you a more effective and confident React developer.