🔥 0
0
Lesson 8 of 10 25 min +125 XP

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

ConceptSyntaxUse Case
Basic genericType placeholder
ConstraintLimit allowed types
Multiple paramsTransform types
PartialAll optionalUpdate functions
PickSelect fieldsSubset of interface
OmitExclude fieldsCreate without ID
RecordObject typeLookup tables

Next up: TypeScript with React - typing components, props, hooks, and events!

Type Narrowing