Prerequisites
- Basic understanding of JavaScript 📝
- TypeScript installation ⚡
- VS Code or preferred IDE 💻
- React fundamentals 🎨
- JSX syntax knowledge ⚡
What you'll learn
- Set up TypeScript with React projects 🏗️
- Create type-safe React components 🎯
- Handle props and state with TypeScript ✨
- Work with events and hooks type-safely 🛡️
- Build production-ready React + TypeScript apps 🚀
🎯 Introduction
Welcome to the ultimate guide for combining TypeScript with React! 🎉 If you’ve ever wondered how to make your React apps bulletproof with type safety, you’re in the right place!
React is amazing for building user interfaces 🎨, but when you add TypeScript to the mix, you get superpowers! 🦸♂️ Imagine catching bugs before they happen, getting incredible autocomplete in your IDE, and having self-documenting components that make your team smile.
In this tutorial, we’ll transform you from a React developer into a React + TypeScript wizard! ✨ You’ll learn how to create type-safe components, handle props like a pro, and build applications that scale beautifully.
By the end, you’ll be writing React code with confidence, knowing TypeScript has your back! Let’s dive in! 🏊♂️
📚 Understanding TypeScript with React
🤔 What Makes This Combo Special?
Think of React as a talented artist 🎨 and TypeScript as a skilled art teacher 👩🏫. React can create beautiful interfaces, but TypeScript guides it to avoid common mistakes and creates masterpieces!
In practical terms, TypeScript brings these superpowers to React:
- ✨ Type Safety: Catch prop errors before runtime
- 🚀 Better Developer Experience: Amazing autocomplete and refactoring
- 🛡️ Self-Documenting Code: Props tell their own story
- 🔧 Fearless Refactoring: Change code with confidence
💡 Why Use TypeScript with React?
Here’s why React developers fall in love with TypeScript:
- Component Contracts 📋: Props become clear specifications
- Event Handling ⚡: No more guessing event types
- State Management 🗃️: State updates become predictable
- Hook Safety 🪝: Custom hooks with proper typing
- Team Collaboration 🤝: Code becomes self-explanatory
Real-world example: Imagine building a user profile component 👤. With TypeScript, everyone knows exactly what props to pass, what events are available, and how state changes work!
🔧 Basic Setup and Configuration
📝 Creating a New React + TypeScript Project
Let’s start with a fresh project:
# 🚀 Create new React app with TypeScript
npx create-react-app my-awesome-app --template typescript
# 🏃♂️ Or with Vite (faster!)
npm create vite@latest my-awesome-app -- --template react-ts
# 📦 Install dependencies
cd my-awesome-app
npm install
💡 Pro Tip: Vite is super fast and gives you a better development experience! 🚀
🎯 Basic TypeScript Configuration
Your tsconfig.json
should look like this:
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "es6"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true, // 🛡️ Enable all strict checks
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx" // 🎨 Modern JSX transform
},
"include": [
"src"
]
}
💡 Practical Examples
🛒 Example 1: Building a Product Card Component
Let’s create a type-safe product card:
import React from 'react';
// 🏷️ Define our product type
interface Product {
id: string;
name: string;
price: number;
inStock: boolean;
category: 'electronics' | 'clothing' | 'books';
emoji: string; // Every product needs an emoji!
rating?: number; // Optional rating
}
// 🎨 Component props interface
interface ProductCardProps {
product: Product;
onAddToCart: (productId: string) => void;
onViewDetails: (product: Product) => void;
showRating?: boolean;
}
// 📱 Our type-safe component
const ProductCard: React.FC<ProductCardProps> = ({
product,
onAddToCart,
onViewDetails,
showRating = true
}) => {
// 🎯 Handle add to cart
const handleAddToCart = () => {
if (product.inStock) {
onAddToCart(product.id);
console.log(`Added ${product.emoji} ${product.name} to cart!`);
}
};
// 📊 Handle view details
const handleViewDetails = () => {
onViewDetails(product);
};
return (
<div className="product-card">
<div className="product-header">
<span className="product-emoji">{product.emoji}</span>
<h3>{product.name}</h3>
</div>
<div className="product-info">
<p className="price">${product.price.toFixed(2)}</p>
<p className="category">Category: {product.category}</p>
{showRating && product.rating && (
<div className="rating">
{'⭐'.repeat(Math.floor(product.rating))} ({product.rating})
</div>
)}
</div>
<div className="product-actions">
<button
onClick={handleAddToCart}
disabled={!product.inStock}
className={product.inStock ? "btn-primary" : "btn-disabled"}
>
{product.inStock ? "🛒 Add to Cart" : "😞 Out of Stock"}
</button>
<button onClick={handleViewDetails} className="btn-secondary">
👁️ View Details
</button>
</div>
</div>
);
};
// 🎮 Usage example
const App: React.FC = () => {
const sampleProduct: Product = {
id: "1",
name: "TypeScript Handbook",
price: 29.99,
inStock: true,
category: "books",
emoji: "📘",
rating: 4.8
};
const handleAddToCart = (productId: string) => {
console.log(`Product ${productId} added to cart! 🎉`);
};
const handleViewDetails = (product: Product) => {
console.log(`Viewing details for: ${product.name} ${product.emoji}`);
};
return (
<div className="app">
<h1>🛍️ My Awesome Store</h1>
<ProductCard
product={sampleProduct}
onAddToCart={handleAddToCart}
onViewDetails={handleViewDetails}
/>
</div>
);
};
export default App;
🎮 Example 2: Game Scoreboard with Hooks
Let’s build a type-safe game scoreboard:
import React, { useState, useEffect, useCallback } from 'react';
// 🏆 Player type definition
interface Player {
id: string;
name: string;
score: number;
avatar: string; // Emoji avatar
isActive: boolean;
}
// 🎯 Game statistics
interface GameStats {
totalPlayers: number;
highestScore: number;
averageScore: number;
gameStartTime: Date;
}
// 🎮 Custom hook for game logic
const useGameLogic = (initialPlayers: Player[]) => {
const [players, setPlayers] = useState<Player[]>(initialPlayers);
const [gameStats, setGameStats] = useState<GameStats>({
totalPlayers: initialPlayers.length,
highestScore: 0,
averageScore: 0,
gameStartTime: new Date()
});
// 🎯 Add points to a player
const addPoints = useCallback((playerId: string, points: number) => {
setPlayers(prevPlayers =>
prevPlayers.map(player =>
player.id === playerId
? { ...player, score: player.score + points }
: player
)
);
}, []);
// 🔄 Reset all scores
const resetScores = useCallback(() => {
setPlayers(prevPlayers =>
prevPlayers.map(player => ({ ...player, score: 0 }))
);
}, []);
// 📊 Update stats when players change
useEffect(() => {
const scores = players.map(p => p.score);
const total = scores.reduce((sum, score) => sum + score, 0);
setGameStats(prev => ({
...prev,
totalPlayers: players.length,
highestScore: Math.max(...scores, 0),
averageScore: players.length > 0 ? total / players.length : 0
}));
}, [players]);
return { players, gameStats, addPoints, resetScores };
};
// 🏆 Scoreboard component
const GameScoreboard: React.FC = () => {
const initialPlayers: Player[] = [
{ id: '1', name: 'Alice', score: 0, avatar: '👩💻', isActive: true },
{ id: '2', name: 'Bob', score: 0, avatar: '👨🎨', isActive: true },
{ id: '3', name: 'Charlie', score: 0, avatar: '🧑🚀', isActive: true },
];
const { players, gameStats, addPoints, resetScores } = useGameLogic(initialPlayers);
// 🎯 Handle point addition
const handleAddPoints = (playerId: string, points: number) => {
addPoints(playerId, points);
console.log(`✨ Added ${points} points!`);
};
// 🏆 Find the leader
const leader = players.reduce((prev, current) =>
prev.score > current.score ? prev : current
);
return (
<div className="game-scoreboard">
<h2>🎮 Game Scoreboard</h2>
{/* 📊 Game Statistics */}
<div className="game-stats">
<h3>📊 Game Stats</h3>
<p>👥 Players: {gameStats.totalPlayers}</p>
<p>🏆 Highest Score: {gameStats.highestScore}</p>
<p>📈 Average Score: {gameStats.averageScore.toFixed(1)}</p>
<p>⏰ Started: {gameStats.gameStartTime.toLocaleTimeString()}</p>
</div>
{/* 🏆 Current Leader */}
<div className="leader-section">
<h3>👑 Current Leader</h3>
<p>{leader.avatar} {leader.name} - {leader.score} points!</p>
</div>
{/* 👥 Player List */}
<div className="players-section">
<h3>👥 Players</h3>
{players.map((player) => (
<div key={player.id} className="player-card">
<span className="avatar">{player.avatar}</span>
<span className="name">{player.name}</span>
<span className="score">{player.score} points</span>
<div className="actions">
<button onClick={() => handleAddPoints(player.id, 10)}>
+10 🎯
</button>
<button onClick={() => handleAddPoints(player.id, 25)}>
+25 🚀
</button>
<button onClick={() => handleAddPoints(player.id, 50)}>
+50 ⭐
</button>
</div>
</div>
))}
</div>
{/* 🔄 Reset Button */}
<button onClick={resetScores} className="reset-btn">
🔄 Reset All Scores
</button>
</div>
);
};
export default GameScoreboard;
🚀 Advanced Concepts
🧙♂️ Advanced Pattern 1: Generic Components
Create reusable, type-safe components:
// 🎯 Generic list component that works with any data type
interface ListItem {
id: string;
}
interface GenericListProps<T extends ListItem> {
items: T[];
renderItem: (item: T) => React.ReactNode;
onItemClick?: (item: T) => void;
emptyMessage?: string;
}
function GenericList<T extends ListItem>({
items,
renderItem,
onItemClick,
emptyMessage = "No items found 😔"
}: GenericListProps<T>) {
if (items.length === 0) {
return <div className="empty-state">{emptyMessage}</div>;
}
return (
<div className="generic-list">
{items.map((item) => (
<div
key={item.id}
className="list-item"
onClick={() => onItemClick?.(item)}
>
{renderItem(item)}
</div>
))}
</div>
);
}
// 🎮 Usage with different data types
interface TodoItem extends ListItem {
title: string;
completed: boolean;
emoji: string;
}
interface UserItem extends ListItem {
name: string;
email: string;
avatar: string;
}
const MyApp: React.FC = () => {
const todos: TodoItem[] = [
{ id: '1', title: 'Learn TypeScript', completed: false, emoji: '📘' },
{ id: '2', title: 'Build React App', completed: true, emoji: '🚀' },
];
const users: UserItem[] = [
{ id: '1', name: 'Alice', email: '[email protected]', avatar: '👩💻' },
{ id: '2', name: 'Bob', email: '[email protected]', avatar: '👨🎨' },
];
return (
<div>
{/* 📝 Todo List */}
<GenericList
items={todos}
renderItem={(todo) => (
<span>
{todo.emoji} {todo.title}
{todo.completed ? ' ✅' : ' ⏳'}
</span>
)}
onItemClick={(todo) => console.log(`Clicked: ${todo.title}`)}
emptyMessage="No todos yet! 🎉"
/>
{/* 👥 User List */}
<GenericList
items={users}
renderItem={(user) => (
<div>
{user.avatar} {user.name} ({user.email})
</div>
)}
onItemClick={(user) => console.log(`Selected: ${user.name}`)}
/>
</div>
);
};
🏗️ Advanced Pattern 2: Context with TypeScript
Type-safe React Context:
import React, { createContext, useContext, useReducer, ReactNode } from 'react';
// 🎯 Theme types
type Theme = 'light' | 'dark' | 'cosmic';
interface ThemeState {
theme: Theme;
isAnimated: boolean;
primaryColor: string;
}
// 🎨 Theme actions
type ThemeAction =
| { type: 'SET_THEME'; payload: Theme }
| { type: 'TOGGLE_ANIMATION' }
| { type: 'SET_PRIMARY_COLOR'; payload: string };
// 🌟 Context type
interface ThemeContextType {
state: ThemeState;
dispatch: React.Dispatch<ThemeAction>;
// 💡 Helper functions
setTheme: (theme: Theme) => void;
toggleAnimation: () => void;
setPrimaryColor: (color: string) => void;
}
// 🚀 Create context with proper typing
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
// 🔄 Reducer function
const themeReducer = (state: ThemeState, action: ThemeAction): ThemeState => {
switch (action.type) {
case 'SET_THEME':
return { ...state, theme: action.payload };
case 'TOGGLE_ANIMATION':
return { ...state, isAnimated: !state.isAnimated };
case 'SET_PRIMARY_COLOR':
return { ...state, primaryColor: action.payload };
default:
return state;
}
};
// 🏗️ Provider component
interface ThemeProviderProps {
children: ReactNode;
}
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
const [state, dispatch] = useReducer(themeReducer, {
theme: 'light',
isAnimated: true,
primaryColor: '#007bff'
});
// 💡 Helper functions
const setTheme = (theme: Theme) => {
dispatch({ type: 'SET_THEME', payload: theme });
};
const toggleAnimation = () => {
dispatch({ type: 'TOGGLE_ANIMATION' });
};
const setPrimaryColor = (color: string) => {
dispatch({ type: 'SET_PRIMARY_COLOR', payload: color });
};
const value: ThemeContextType = {
state,
dispatch,
setTheme,
toggleAnimation,
setPrimaryColor
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
};
// 🪝 Custom hook for using theme
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
// 🎨 Component using the theme
const ThemedButton: React.FC<{ children: ReactNode }> = ({ children }) => {
const { state, setTheme } = useTheme();
const buttonStyle = {
backgroundColor: state.primaryColor,
color: state.theme === 'dark' ? '#fff' : '#000',
animation: state.isAnimated ? 'pulse 2s infinite' : 'none'
};
return (
<button
style={buttonStyle}
onClick={() => setTheme(state.theme === 'light' ? 'dark' : 'light')}
>
{children} {state.theme === 'light' ? '🌞' : '🌙'}
</button>
);
};
⚠️ Common Pitfalls and Solutions
😱 Pitfall 1: Event Handler Types
// ❌ Wrong - using 'any' loses type safety
const handleClick = (event: any) => {
console.log(event.target.value); // 💥 No autocomplete, no safety!
};
// ✅ Correct - proper event typing
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
console.log(event.currentTarget.textContent); // ✨ Full type safety!
};
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value); // 🎯 Perfect autocomplete!
};
🤯 Pitfall 2: Props with Children
// ❌ Dangerous - missing children type
interface BadComponentProps {
title: string;
// children is missing! 😱
}
// ✅ Safe - proper children typing
interface GoodComponentProps {
title: string;
children: React.ReactNode; // 🛡️ Accepts any valid React content
}
// 🚀 Even better - with PropsWithChildren
interface BestComponentProps {
title: string;
isVisible?: boolean;
}
const MyComponent: React.FC<React.PropsWithChildren<BestComponentProps>> = ({
title,
isVisible = true,
children
}) => {
if (!isVisible) return null;
return (
<div>
<h2>{title}</h2>
{children}
</div>
);
};
🔥 Pitfall 3: State with Complex Types
// ❌ Wrong - mutable array updates
const [items, setItems] = useState<string[]>([]);
const addItem = (newItem: string) => {
items.push(newItem); // 💥 Mutating state directly!
setItems(items); // React won't re-render!
};
// ✅ Correct - immutable updates
const [items, setItems] = useState<string[]>([]);
const addItem = (newItem: string) => {
setItems(prevItems => [...prevItems, newItem]); // ✨ Immutable update!
};
// 🚀 Even better - with proper typing for complex state
interface TodoState {
items: Todo[];
filter: 'all' | 'completed' | 'pending';
isLoading: boolean;
}
const [todoState, setTodoState] = useState<TodoState>({
items: [],
filter: 'all',
isLoading: false
});
const addTodo = (newTodo: Todo) => {
setTodoState(prev => ({
...prev,
items: [...prev.items, newTodo]
}));
};
🛠️ Best Practices
- 🎯 Use Interfaces for Props: Always define clear prop interfaces
- 📝 Leverage Type Inference: Let TypeScript infer when it’s obvious
- 🛡️ Enable Strict Mode: Use all TypeScript safety features
- 🎨 Use Meaningful Generic Names:
<TData>
not<T>
- ✨ Create Reusable Types: Share types across components
- 🔧 Use as const for Arrays: Make arrays readonly when needed
- 🚀 Embrace Union Types: Use them for component variants
// 🌟 Example of best practices
interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger';
size: 'small' | 'medium' | 'large';
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
loading?: boolean;
}
const Button: React.FC<ButtonProps> = ({
variant,
size,
children,
onClick,
disabled = false,
loading = false
}) => {
const baseClasses = 'btn';
const variantClasses = `btn-${variant}`;
const sizeClasses = `btn-${size}`;
return (
<button
className={`${baseClasses} ${variantClasses} ${sizeClasses}`}
onClick={onClick}
disabled={disabled || loading}
>
{loading ? '⏳ Loading...' : children}
</button>
);
};
🧪 Hands-On Exercise
🎯 Challenge: Build a Type-Safe Todo App
Create a complete todo application with full TypeScript integration:
📋 Requirements:
- ✅ Todo items with title, completed status, and priority
- 🏷️ Categories for todos (work, personal, shopping)
- 👤 User assignment feature
- 📅 Due dates with reminders
- 🎨 Each todo needs an emoji!
- 🔍 Search and filter functionality
- 📊 Statistics dashboard
🚀 Bonus Points:
- Add drag-and-drop reordering
- Implement local storage persistence
- Create a theme switcher
- Add keyboard shortcuts
💡 Solution
🔍 Click to see the complete solution
import React, { useState, useEffect, useMemo } from 'react';
// 🎯 Core types
interface Todo {
id: string;
title: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
category: 'work' | 'personal' | 'shopping';
emoji: string;
dueDate?: Date;
assignee?: string;
createdAt: Date;
}
type FilterType = 'all' | 'completed' | 'pending' | 'overdue';
type SortType = 'created' | 'dueDate' | 'priority' | 'title';
interface TodoStats {
total: number;
completed: number;
pending: number;
overdue: number;
completionRate: number;
}
// 🎨 Todo App Component
const TodoApp: React.FC = () => {
// 🗃️ State management
const [todos, setTodos] = useState<Todo[]>([]);
const [filter, setFilter] = useState<FilterType>('all');
const [sort, setSort] = useState<SortType>('created');
const [searchQuery, setSearchQuery] = useState<string>('');
const [newTodo, setNewTodo] = useState<Partial<Todo>>({
title: '',
priority: 'medium',
category: 'personal',
emoji: '📝'
});
// 📊 Calculate statistics
const stats: TodoStats = useMemo(() => {
const now = new Date();
const completed = todos.filter(t => t.completed).length;
const overdue = todos.filter(t =>
!t.completed && t.dueDate && t.dueDate < now
).length;
return {
total: todos.length,
completed,
pending: todos.length - completed,
overdue,
completionRate: todos.length > 0 ? (completed / todos.length) * 100 : 0
};
}, [todos]);
// 🔍 Filter and sort todos
const filteredTodos = useMemo(() => {
let filtered = todos;
// Apply filter
switch (filter) {
case 'completed':
filtered = todos.filter(t => t.completed);
break;
case 'pending':
filtered = todos.filter(t => !t.completed);
break;
case 'overdue':
const now = new Date();
filtered = todos.filter(t =>
!t.completed && t.dueDate && t.dueDate < now
);
break;
}
// Apply search
if (searchQuery) {
filtered = filtered.filter(t =>
t.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
t.category.toLowerCase().includes(searchQuery.toLowerCase()) ||
t.assignee?.toLowerCase().includes(searchQuery.toLowerCase())
);
}
// Apply sort
filtered.sort((a, b) => {
switch (sort) {
case 'title':
return a.title.localeCompare(b.title);
case 'priority':
const priorityOrder = { low: 1, medium: 2, high: 3 };
return priorityOrder[b.priority] - priorityOrder[a.priority];
case 'dueDate':
if (!a.dueDate && !b.dueDate) return 0;
if (!a.dueDate) return 1;
if (!b.dueDate) return -1;
return a.dueDate.getTime() - b.dueDate.getTime();
default:
return b.createdAt.getTime() - a.createdAt.getTime();
}
});
return filtered;
}, [todos, filter, sort, searchQuery]);
// ➕ Add new todo
const addTodo = () => {
if (!newTodo.title?.trim()) return;
const todo: Todo = {
id: Date.now().toString(),
title: newTodo.title,
completed: false,
priority: newTodo.priority || 'medium',
category: newTodo.category || 'personal',
emoji: newTodo.emoji || '📝',
dueDate: newTodo.dueDate,
assignee: newTodo.assignee,
createdAt: new Date()
};
setTodos(prev => [todo, ...prev]);
setNewTodo({ title: '', priority: 'medium', category: 'personal', emoji: '📝' });
};
// ✅ Toggle todo completion
const toggleTodo = (id: string) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
// 🗑️ Delete todo
const deleteTodo = (id: string) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
};
// 💾 Load from localStorage on mount
useEffect(() => {
const saved = localStorage.getItem('todos');
if (saved) {
const parsedTodos = JSON.parse(saved).map((todo: any) => ({
...todo,
createdAt: new Date(todo.createdAt),
dueDate: todo.dueDate ? new Date(todo.dueDate) : undefined
}));
setTodos(parsedTodos);
}
}, []);
// 💾 Save to localStorage when todos change
useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
return (
<div className="todo-app">
<h1>🎯 TypeScript Todo App</h1>
{/* 📊 Statistics Dashboard */}
<div className="stats-dashboard">
<h2>📊 Dashboard</h2>
<div className="stats-grid">
<div className="stat-card">
<h3>📝 Total</h3>
<p>{stats.total}</p>
</div>
<div className="stat-card">
<h3>✅ Completed</h3>
<p>{stats.completed}</p>
</div>
<div className="stat-card">
<h3>⏳ Pending</h3>
<p>{stats.pending}</p>
</div>
<div className="stat-card">
<h3>⚠️ Overdue</h3>
<p>{stats.overdue}</p>
</div>
<div className="stat-card">
<h3>📈 Completion Rate</h3>
<p>{stats.completionRate.toFixed(1)}%</p>
</div>
</div>
</div>
{/* ➕ Add New Todo */}
<div className="add-todo-section">
<h2>➕ Add New Todo</h2>
<div className="add-todo-form">
<input
type="text"
placeholder="What needs to be done? 🤔"
value={newTodo.title || ''}
onChange={(e) => setNewTodo(prev => ({ ...prev, title: e.target.value }))}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
/>
<select
value={newTodo.category}
onChange={(e) => setNewTodo(prev => ({
...prev,
category: e.target.value as Todo['category']
}))}
>
<option value="work">💼 Work</option>
<option value="personal">👤 Personal</option>
<option value="shopping">🛒 Shopping</option>
</select>
<select
value={newTodo.priority}
onChange={(e) => setNewTodo(prev => ({
...prev,
priority: e.target.value as Todo['priority']
}))}
>
<option value="low">🟢 Low</option>
<option value="medium">🟡 Medium</option>
<option value="high">🔴 High</option>
</select>
<input
type="text"
placeholder="Emoji 🎨"
value={newTodo.emoji}
onChange={(e) => setNewTodo(prev => ({ ...prev, emoji: e.target.value }))}
maxLength={2}
/>
<input
type="date"
value={newTodo.dueDate?.toISOString().split('T')[0] || ''}
onChange={(e) => setNewTodo(prev => ({
...prev,
dueDate: e.target.value ? new Date(e.target.value) : undefined
}))}
/>
<button onClick={addTodo}>✨ Add Todo</button>
</div>
</div>
{/* 🔍 Search and Filters */}
<div className="filters-section">
<input
type="text"
placeholder="🔍 Search todos..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<select
value={filter}
onChange={(e) => setFilter(e.target.value as FilterType)}
>
<option value="all">🌟 All</option>
<option value="pending">⏳ Pending</option>
<option value="completed">✅ Completed</option>
<option value="overdue">⚠️ Overdue</option>
</select>
<select
value={sort}
onChange={(e) => setSort(e.target.value as SortType)}
>
<option value="created">📅 Created Date</option>
<option value="dueDate">⏰ Due Date</option>
<option value="priority">🎯 Priority</option>
<option value="title">📝 Title</option>
</select>
</div>
{/* 📋 Todo List */}
<div className="todos-section">
<h2>📋 Your Todos ({filteredTodos.length})</h2>
{filteredTodos.length === 0 ? (
<div className="empty-state">
{searchQuery ? '🔍 No todos match your search' : '🎉 No todos yet!'}
</div>
) : (
<div className="todos-list">
{filteredTodos.map((todo) => {
const isOverdue = !todo.completed && todo.dueDate && todo.dueDate < new Date();
const priorityEmoji = {
low: '🟢',
medium: '🟡',
high: '🔴'
};
return (
<div key={todo.id} className={`todo-item ${todo.completed ? 'completed' : ''} ${isOverdue ? 'overdue' : ''}`}>
<div className="todo-main">
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span className="todo-emoji">{todo.emoji}</span>
<span className="todo-title">{todo.title}</span>
</div>
<div className="todo-meta">
<span className="category">🏷️ {todo.category}</span>
<span className="priority">{priorityEmoji[todo.priority]} {todo.priority}</span>
{todo.dueDate && (
<span className={`due-date ${isOverdue ? 'overdue' : ''}`}>
📅 {todo.dueDate.toLocaleDateString()}
</span>
)}
{todo.assignee && (
<span className="assignee">👤 {todo.assignee}</span>
)}
</div>
<button
onClick={() => deleteTodo(todo.id)}
className="delete-btn"
>
🗑️ Delete
</button>
</div>
);
})}
</div>
)}
</div>
</div>
);
};
export default TodoApp;
🎓 Key Takeaways
Congratulations! 🎉 You’ve mastered TypeScript with React! Here’s what you can now do:
- ✅ Set up TypeScript with React from scratch 🏗️
- ✅ Create type-safe components with proper prop interfaces 🎯
- ✅ Handle events and state with full type safety 🛡️
- ✅ Build generic, reusable components that scale 🚀
- ✅ Use advanced patterns like Context and custom hooks ✨
- ✅ Avoid common pitfalls that trip up developers 🔧
- ✅ Apply best practices for maintainable code 📚
Remember: TypeScript + React = Developer Superpowers! 🦸♂️ You now have the tools to build robust, scalable applications with confidence.
🤝 Next Steps
You’re ready to conquer the React + TypeScript world! 🌟
Here’s your roadmap:
- 💻 Practice: Build the todo app exercise and add your own features
- 🏗️ Build Projects: Create a portfolio project showcasing your skills
- 📚 Advanced Topics: Explore our next tutorial on “Next.js with TypeScript”
- 🌟 Share: Show off your type-safe React components to the world!
- 🤝 Contribute: Help others learn by sharing your journey
Pro tips for continued growth:
- 🔧 Experiment with advanced TypeScript features in React
- 📱 Try React Native with TypeScript for mobile apps
- 🧪 Explore testing libraries like React Testing Library with TypeScript
- 🎨 Build component libraries with Storybook and TypeScript
You’ve got the foundation - now go build amazing things! 🚀
Remember: Every React + TypeScript expert was once where you are now. Keep coding, keep learning, and most importantly, have fun creating awesome user experiences! 🎉
Happy coding with React and TypeScript! 🎉🚀✨