Generics
Generics let you write code that works with any type while keeping type safety. Think of them as "type variables" - placeholders that get filled in when you use the code.
The Problem Generics Solve
Without generics, you'd need to write the same function for every type:
function getFirstProduct(items: Product[]): Product | undefined {
return items[0];
}
function getFirstOrder(items: Order[]): Order | undefined {
return items[0];
}
function getFirstCustomer(items: Customer[]): Customer | undefined {
return items[0];
}
// This is repetitive!
With generics:
function getFirst<T>(items: T[]): T | undefined {
return items[0];
}
// Works with any type!
const firstProduct = getFirst(products); // Product | undefined
const firstOrder = getFirst(orders); // Order | undefined
const firstCustomer = getFirst(customers); // Customer | undefined
Basic Generic Syntax
The is a type parameter - a placeholder for a type:
// Type parameter
// ↓
function identity<T>(value: T): T {
return value;
}
// TypeScript infers T from the argument
identity("hello"); // T = string, returns string
identity(42); // T = number, returns number
identity({ x: 1 }); // T = { x: number }, returns { x: number }
// Or specify explicitly
identity<string>("hello");
identity<number>(42);
---
Generic Functions for E-commerce
Find Item by ID
function findById<T extends { id: string }>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
// Works with Products
const products: Product[] = [
{ id: "SKU-001", name: "Mouse", price: 29.99 },
{ id: "SKU-002", name: "Keyboard", price: 99.99 }
];
const mouse = findById(products, "SKU-001"); // Product | undefined
// Works with Orders
const orders: Order[] = [
{ id: "ORD-001", total: 150, status: "shipped" },
{ id: "ORD-002", total: 89, status: "pending" }
];
const order = findById(orders, "ORD-001"); // Order | undefined
// Works with Customers
const customers: Customer[] = [...];
const customer = findById(customers, "CUST-001"); // Customer | undefined
Group Items
function groupBy<T>(items: T[], keyFn: (item: T) => string): Record<string, T[]> {
return items.reduce((groups, item) => {
const key = keyFn(item);
groups[key] = groups[key] || [];
groups[key].push(item);
return groups;
}, {} as Record<string, T[]>);
}
// Group products by category
const productsByCategory = groupBy(products, p => p.category);
// { "electronics": [...], "clothing": [...] }
// Group orders by status
const ordersByStatus = groupBy(orders, o => o.status);
// { "pending": [...], "shipped": [...] }
---
Generic Interfaces
API Response Wrapper
A common pattern - wrap any data type in a consistent response structure:
interface ApiResponse<T> {
data: T;
status: "success" | "error";
timestamp: Date;
meta?: {
page?: number;
totalPages?: number;
totalItems?: number;
};
}
// Response containing a single product
type ProductResponse = ApiResponse<Product>;
// Response containing an array of products
type ProductListResponse = ApiResponse<Product[]>;
// Response containing order details
type OrderResponse = ApiResponse<Order>;
// Usage
async function fetchProduct(id: string): Promise<ApiResponse<Product>> {
const response = await fetch(`/api/products/${id}`);
return response.json();
}
const result = await fetchProduct("SKU-001");
console.log(result.data.name); // TypeScript knows data is Product
Paginated Response
interface PaginatedResponse<T> {
items: T[];
pagination: {
page: number;
pageSize: number;
totalItems: number;
totalPages: number;
hasNext: boolean;
hasPrevious: boolean;
};
}
async function fetchProducts(page: number): Promise<PaginatedResponse<Product>> {
const response = await fetch(`/api/products?page=${page}`);
return response.json();
}
const products = await fetchProducts(1);
products.items.forEach(p => console.log(p.name));
console.log(`Page ${products.pagination.page} of ${products.pagination.totalPages}`);
---
Generic Constraints
Limit what types can be used with extends:
// T must have an 'id' property
function updateById<T extends { id: string }>(
items: T[],
id: string,
updates: Partial<T>
): T[] {
return items.map(item =>
item.id === id ? { ...item, ...updates } : item
);
}
// T must have 'price' property
function calculateTotal<T extends { price: number }>(items: T[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
// Works with any type that has price
calculateTotal(products); // ✅ Products have price
calculateTotal(cartItems.map(ci => ci.product)); // ✅
calculateTotal(customers); // ❌ Error: Customer doesn't have 'price'
---
Multiple Type Parameters
// Map one type to another
function mapItems<T, U>(items: T[], transform: (item: T) => U): U[] {
return items.map(transform);
}
// Convert products to display format
interface ProductDisplay {
label: string;
formattedPrice: string;
}
const displayProducts = mapItems(products, p => ({
label: p.name,
formattedPrice: `${p.price.toFixed(2)}`
}));
// displayProducts is ProductDisplay[]
// Key-value pair
function createLookup<T, K extends keyof T>(
items: T[],
key: K
): Map<T[K], T> {
const map = new Map<T[K], T>();
items.forEach(item => map.set(item[key], item));
return map;
}
const productById = createLookup(products, "id");
// Map<string, Product>
const productByName = createLookup(products, "name");
// Map<string, Product>
---
Built-in Utility Types
TypeScript provides generic utility types out of the box:
Partial - All Properties Optional
interface Product {
id: string;
name: string;
price: number;
description: string;
}
// All fields optional - great for updates
function updateProduct(id: string, updates: Partial<Product>): void {
// updates could have any combination of Product fields
}
updateProduct("SKU-001", { price: 24.99 }); // Just update price
updateProduct("SKU-001", { name: "New Name", description: "Updated" });
Required - All Properties Required
interface Config {
apiUrl?: string;
timeout?: number;
retries?: number;
}
// Make all optional fields required
function initializeApp(config: Required<Config>): void {
// All fields guaranteed to exist
}
Pick - Select Specific Properties
interface Product {
id: string;
name: string;
price: number;
description: string;
imageUrl: string;
stock: number;
}
// Only what we need for the cart display
type CartProduct = Pick<Product, "id" | "name" | "price">;
const cartItem: CartProduct = {
id: "SKU-001",
name: "Mouse",
price: 29.99
};
Omit - Exclude Properties
// Everything except stock and imageUrl
type ProductInput = Omit<Product, "id" | "stock">;
// For creating new products (id auto-generated, stock starts at 0)
function createProduct(input: ProductInput): Product {
return {
...input,
id: generateId(),
stock: 0
};
}
Record - Object Type
// Inventory by product ID
type Inventory = Record<string, number>;
const stock: Inventory = {
"SKU-001": 50,
"SKU-002": 100
};
// Settings by category
type CategorySettings = Record<Category, { featured: boolean; sortOrder: number }>;
---
Putting It All Together
// Generic cart operations
interface CartItem<T> {
item: T;
quantity: number;
}
interface Cart<T> {
items: CartItem<T>[];
addItem: (item: T, quantity: number) => void;
removeItem: (itemId: string) => void;
getTotal: (priceExtractor: (item: T) => number) => number;
}
function createCart<T extends { id: string }>(): Cart<T> {
const items: CartItem<T>[] = [];
return {
items,
addItem(item: T, quantity: number = 1) {
const existing = items.find(ci => ci.item.id === item.id);
if (existing) {
existing.quantity += quantity;
} else {
items.push({ item, quantity });
}
},
removeItem(itemId: string) {
const index = items.findIndex(ci => ci.item.id === itemId);
if (index >= 0) items.splice(index, 1);
},
getTotal(priceExtractor: (item: T) => number): number {
return items.reduce(
(sum, ci) => sum + priceExtractor(ci.item) * ci.quantity,
0
);
}
};
}
// Use with products
const productCart = createCart<Product>();
productCart.addItem({ id: "1", name: "Mouse", price: 29.99 }, 2);
const total = productCart.getTotal(p => p.price); // 59.98
---
Type Challenges
Challenge 1: Generic Filter
Create a generic filter function:
// Filter items where a specific property equals a value
function filterByProperty<T, K extends keyof T>(
items: T[],
property: K,
value: T[K]
): T[] {
// Your code here
}
// Usage:
filterByProperty(products, "category", "electronics");
filterByProperty(orders, "status", "shipped");
Solution
function filterByProperty<T, K extends keyof T>(
items: T[],
property: K,
value: T[K]
): T[] {
return items.filter(item => item[property] === value);
}
Challenge 2: Generic State Container
Create a generic state container with get/set:
// Your code here
// Usage:
const cartState = createState<Cart>({ items: [], coupon: null });
cartState.get(); // Returns Cart
cartState.set({ items: [...], coupon: "SAVE10" });
cartState.update(cart => ({ ...cart, coupon: null }));
Solution
interface State<T> {
get: () => T;
set: (value: T) => void;
update: (updater: (current: T) => T) => void;
}
function createState<T>(initial: T): State<T> {
let value = initial;
return {
get: () => value,
set: (newValue: T) => { value = newValue; },
update: (updater: (current: T) => T) => { value = updater(value); }
};
}
---
Key Takeaways
| Concept | Syntax | Use Case |
|---|---|---|
| Basic generic | | Type placeholder |
| Constraint | | Limit allowed types |
| Multiple params | | Transform types |
Partial | All optional | Update functions |
Pick | Select fields | Subset of interface |
Omit | Exclude fields | Create without ID |
Record | Object type | Lookup tables |
Next up: TypeScript with React - typing components, props, hooks, and events!