TypeScript with React
React + TypeScript is the industry standard. Let's build type-safe e-commerce components.
Typing Component Props
Basic Props
// Define props interface
interface ProductCardProps {
name: string;
price: number;
imageUrl: string;
inStock: boolean;
}
// Use in component
function ProductCard({ name, price, imageUrl, inStock }: ProductCardProps) {
return (
<div className="product-card">
<img src={imageUrl} alt={name} />
<h3>{name}</h3>
<p>${price.toFixed(2)}</p>
{!inStock && <span className="badge">Out of Stock</span>}
</div>
);
}
// TypeScript ensures correct props
<ProductCard
name="Wireless Mouse"
price={29.99}
imageUrl="/images/mouse.jpg"
inStock={true}
/>
// ❌ Error: missing 'inStock' property
<ProductCard
name="Keyboard"
price={99.99}
imageUrl="/images/keyboard.jpg"
/>
Optional Props
interface ProductCardProps {
name: string;
price: number;
imageUrl?: string; // Optional
inStock?: boolean; // Optional with default
discountPercent?: number; // Optional
}
function ProductCard({
name,
price,
imageUrl = "/images/placeholder.jpg", // Default value
inStock = true, // Default value
discountPercent
}: ProductCardProps) {
const finalPrice = discountPercent
? price * (1 - discountPercent / 100)
: price;
return (
<div className="product-card">
<img src={imageUrl} alt={name} />
<h3>{name}</h3>
{discountPercent && (
<span className="discount">{discountPercent}% OFF</span>
)}
<p>${finalPrice.toFixed(2)}</p>
</div>
);
}
---
Typing Children
Basic Children
interface CardProps {
title: string;
children: React.ReactNode; // Accepts anything renderable
}
function Card({ title, children }: CardProps) {
return (
<div className="card">
<h2>{title}</h2>
<div className="card-body">{children}</div>
</div>
);
}
// Usage
<Card title="Featured Products">
<ProductCard {...product1} />
<ProductCard {...product2} />
</Card>
Specific Children Types
interface ProductListProps {
children: React.ReactElement<ProductCardProps>[]; // Only ProductCard allowed
}
// Or with PropsWithChildren helper
import { PropsWithChildren } from 'react';
interface CartSummaryProps {
total: number;
}
function CartSummary({ total, children }: PropsWithChildren<CartSummaryProps>) {
return (
<div className="cart-summary">
{children}
<div className="total">Total: ${total.toFixed(2)}</div>
</div>
);
}
---
Typing useState
TypeScript infers state type from initial value:
function ShoppingCart() {
// Inferred: number
const [itemCount, setItemCount] = useState(0);
// Inferred: string
const [couponCode, setCouponCode] = useState("");
// Inferred: boolean
const [isCheckingOut, setIsCheckingOut] = useState(false);
// Need explicit type for complex initial state
const [cart, setCart] = useState<CartItem[]>([]);
// Or for null initial values
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
}
useState with Interfaces
interface CartState {
items: CartItem[];
couponCode: string | null;
isLoading: boolean;
}
function ShoppingCart() {
const [cart, setCart] = useState<CartState>({
items: [],
couponCode: null,
isLoading: false
});
const addItem = (product: Product, quantity: number) => {
setCart(prev => ({
...prev,
items: [...prev.items, { product, quantity }]
}));
};
const applyCoupon = (code: string) => {
setCart(prev => ({ ...prev, couponCode: code }));
};
return (/* ... */);
}
---
Typing Events
Click Events
interface AddToCartButtonProps {
product: Product;
onAddToCart: (product: Product) => void;
}
function AddToCartButton({ product, onAddToCart }: AddToCartButtonProps) {
// Type the event handler
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
onAddToCart(product);
};
return (
<button onClick={handleClick}>
Add to Cart
</button>
);
}
Form Events
interface SearchBarProps {
onSearch: (query: string) => void;
}
function SearchBar({ onSearch }: SearchBarProps) {
const [query, setQuery] = useState("");
// Input change event
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setQuery(event.target.value);
};
// Form submit event
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
onSearch(query);
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={query}
onChange={handleChange}
placeholder="Search products..."
/>
<button type="submit">Search</button>
</form>
);
}
Select Events
interface CategoryFilterProps {
categories: string[];
selected: string;
onChange: (category: string) => void;
}
function CategoryFilter({ categories, selected, onChange }: CategoryFilterProps) {
const handleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
onChange(event.target.value);
};
return (
<select value={selected} onChange={handleChange}>
<option value="">All Categories</option>
{categories.map(cat => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
);
}
---
Typing useEffect
function ProductPage({ productId }: { productId: string }) {
const [product, setProduct] = useState<Product | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// Async function inside useEffect
const fetchProduct = async () => {
try {
setLoading(true);
const response = await fetch(`/api/products/${productId}`);
const data: Product = await response.json();
setProduct(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load");
} finally {
setLoading(false);
}
};
fetchProduct();
}, [productId]); // Dependency array
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!product) return <div>Product not found</div>;
return <ProductCard {...product} />;
}
---
Callback Props Pattern
interface ProductListProps {
products: Product[];
onProductClick: (product: Product) => void;
onAddToCart: (product: Product, quantity: number) => void;
}
function ProductList({ products, onProductClick, onAddToCart }: ProductListProps) {
return (
<div className="product-grid">
{products.map(product => (
<div key={product.id} onClick={() => onProductClick(product)}>
<ProductCard {...product} />
<button onClick={(e) => {
e.stopPropagation();
onAddToCart(product, 1);
}}>
Add to Cart
</button>
</div>
))}
</div>
);
}
// Parent component
function StorePage() {
const [cart, setCart] = useState<CartItem[]>([]);
const handleAddToCart = (product: Product, quantity: number) => {
setCart(prev => [...prev, { product, quantity }]);
};
const handleProductClick = (product: Product) => {
navigate(`/product/${product.id}`);
};
return (
<ProductList
products={products}
onProductClick={handleProductClick}
onAddToCart={handleAddToCart}
/>
);
}
---
Generic Components
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map(item => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
// Usage with products
<List
items={products}
renderItem={(product) => <ProductCard {...product} />}
keyExtractor={(product) => product.id}
/>
// Usage with orders
<List
items={orders}
renderItem={(order) => <OrderSummary {...order} />}
keyExtractor={(order) => order.id}
/>
---
Complete Example: Product Card Component
interface Product {
id: string;
name: string;
price: number;
imageUrl: string;
inStock: boolean;
rating: number;
}
interface ProductCardProps {
product: Product;
onAddToCart: (productId: string, quantity: number) => void;
onWishlist?: (productId: string) => void;
}
function ProductCard({ product, onAddToCart, onWishlist }: ProductCardProps) {
const [quantity, setQuantity] = useState(1);
const [isAdding, setIsAdding] = useState(false);
const handleQuantityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value, 10);
if (value > 0) setQuantity(value);
};
const handleAddToCart = async () => {
setIsAdding(true);
await onAddToCart(product.id, quantity);
setIsAdding(false);
setQuantity(1);
};
return (
<div className="product-card">
<img src={product.imageUrl} alt={product.name} />
<div className="product-info">
<h3>{product.name}</h3>
<div className="rating">{"⭐".repeat(Math.round(product.rating))}</div>
<p className="price">${product.price.toFixed(2)}</p>
</div>
<div className="product-actions">
<input
type="number"
min="1"
value={quantity}
onChange={handleQuantityChange}
disabled={!product.inStock}
/>
<button
onClick={handleAddToCart}
disabled={!product.inStock || isAdding}
>
{isAdding ? "Adding..." : product.inStock ? "Add to Cart" : "Out of Stock"}
</button>
{onWishlist && (
<button onClick={() => onWishlist(product.id)}>
♡ Wishlist
</button>
)}
</div>
</div>
);
}
---
Type Challenges
Challenge 1: Type a Quantity Selector
Complete the types for this component:
interface QuantitySelectorProps {
// Your types here
}
function QuantitySelector({ value, onChange, min, max }: QuantitySelectorProps) {
return (
<div className="quantity-selector">
<button onClick={() => onChange(value - 1)} disabled={value <= min}>-</button>
<span>{value}</span>
<button onClick={() => onChange(value + 1)} disabled={value >= max}>+</button>
</div>
);
}
Solution
interface QuantitySelectorProps {
value: number;
onChange: (newValue: number) => void;
min?: number;
max?: number;
}
function QuantitySelector({
value,
onChange,
min = 1,
max = 99
}: QuantitySelectorProps) {
return (
<div className="quantity-selector">
<button onClick={() => onChange(value - 1)} disabled={value <= min}>-</button>
<span>{value}</span>
<button onClick={() => onChange(value + 1)} disabled={value >= max}>+</button>
</div>
);
}
---
Key Takeaways
| Concept | Syntax | Use Case |
|---|---|---|
| Props interface | interface Props { } | Define component props |
| Children | React.ReactNode | Accept any renderable |
| useState | useState | Explicit state type |
| Events | React.MouseEvent | Type event handlers |
| Callbacks | (item: T) => void | Parent-child communication |
| Generic component | function Comp | Reusable typed components |
You're now ready to build type-safe React applications!