Lab 06: Generics Deep Dive

Objective

Master advanced generic patterns: constrained generics, conditional types, infer, variance, generic utility types, and building reusable generic data structures.

Time

35 minutes

Prerequisites

  • Lab 02 (Functions), Lab 03 (Interfaces)

Tools

  • Docker image: zchencow/innozverse-ts:latest


Lab Instructions

Step 1: Generic Constraints

// keyof constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

// Multiple constraints
function merge<T extends object, U extends object>(a: T, b: U): T & U {
    return { ...a, ...b };
}

// Constraint with interface
interface HasId { id: number; }
interface HasName { name: string; }

function findById<T extends HasId>(items: T[], id: number): T | undefined {
    return items.find(item => item.id === id);
}

function sortByName<T extends HasName>(items: T[]): T[] {
    return [...items].sort((a, b) => a.name.localeCompare(b.name));
}

const products = [
    { id: 1, name: "Surface Pro", price: 864 },
    { id: 2, name: "Surface Pen", price: 49 },
    { id: 3, name: "Office 365", price: 99 },
];

console.log(findById(products, 2));
sortByName(products).forEach(p => console.log(` ${p.name}`));

const merged = merge({ a: 1, b: 2 }, { c: 3, d: "four" });
console.log(merged); // { a: 1, b: 2, c: 3, d: 'four' }

💡 K extends keyof T constrains K to be one of the actual keys of T. This makes getProperty(user, "name") safe — TypeScript knows the return type is string, not unknown. Without this constraint, you'd need to use as unknown casts everywhere.

📸 Verified Output:


Step 2: Conditional Types & infer

💡 infer names a type variable within a conditional type. T extends Promise<infer V> says "if T is a Promise of something, name that something V and use it." This is the only way to extract type components from generic types. ReturnType, Parameters, InstanceType all use infer.

📸 Verified Output:


Step 3: Generic Data Structures

💡 Optional<T> (Java-style Maybe monad) avoids null pointer errors — instead of returning null, return Optional.empty(). Callers use map, filter, and getOrElse to chain operations safely. If the optional is empty, operations are skipped automatically.

📸 Verified Output:


Steps 4–8: Generic Builder, Repository, Middleware, Pipeline, Capstone

💡 Generic caches (TTLCache<K, V>) are reusable for any key/value types — TTLCache<string, User> or TTLCache<number, Product>. The type parameters propagate through all methods, so get() returns V | undefined and TypeScript knows the exact type at each call site.

📸 Verified Output:


Summary

Generics are TypeScript's superpower. You've covered constrained generics, infer for type extraction, Optional<T>, Result<T,E>, generic builders, repositories, middleware pipelines, typed event emitters, and a TTL cache. These patterns appear in every production TypeScript codebase.

Further Reading

Last updated