Skip to main content

Module 20: Advanced Generics

Master advanced generic patterns including constraints, default parameters, variance, and complex generic structures.


1. Generic Constraints

interface HasLength {
length: number;
}

function logLength<T extends HasLength>(item: T): void {
console.log(item.length);
}

logLength("hello"); // ✅ string has length
logLength([1, 2, 3]); // ✅ array has length
// logLength(42); // ❌ number doesn't have length

2. Multiple Generic Constraints

interface Printable {
print(): void;
}

interface Comparable {
compareTo(other: any): number;
}

function process<T extends Printable & Comparable>(item: T): void {
item.print();
item.compareTo(item);
}

3. Generic Constraint with keyof

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

const user = { name: "Alice", age: 25 };
const name = getProperty(user, "name"); // string
const age = getProperty(user, "age"); // number
// getProperty(user, "email"); // ❌ Error

4. Default Generic Parameters

interface APIResponse<T = any> {
data: T;
status: number;
}

let response1: APIResponse = { data: "hello", status: 200 };
let response2: APIResponse<User> = { data: user, status: 200 };

5. Conditional Type with Generics

type Await<T> = T extends Promise<infer U> ? U : T;

async function fetchData<T>(): Promise<T> {
return {} as T;
}

type Data = Await<ReturnType<typeof fetchData<string>>>; // string

6. Generic Class Hierarchies

abstract class Repository<T extends { id: number }> {
protected items: T[] = [];

findById(id: number): T | undefined {
return this.items.find(item => item.id === id);
}

abstract create(data: Omit<T, "id">): T;
}

interface User {
id: number;
name: string;
email: string;
}

class UserRepository extends Repository<User> {
create(data: Omit<User, "id">): User {
const user: User = { id: Date.now(), ...data };
this.items.push(user);
return user;
}
}

7. Generic Factory Pattern

interface Constructor<T> {
new (...args: any[]): T;
}

function createInstance<T>(ctor: Constructor<T>, ...args: any[]): T {
return new ctor(...args);
}

class User {
constructor(public name: string, public age: number) {}
}

const user = createInstance(User, "Alice", 25);

8. Recursive Generic Types

type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object
? DeepPartial<T[P]>
: T[P];
};

interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
}

const partial: DeepPartial<Config> = {
database: {
credentials: {
password: "newpass"
}
}
};

9. Variance in Generics

// Covariance (default for type parameters)
type Covariant<T> = {
getValue(): T;
};

// Contravariance (function parameters)
type Contravariant<T> = {
setValue(value: T): void;
};

// Invariance (both in and out)
type Invariant<T> = {
value: T;
};

10. Generic Builder Pattern

class QueryBuilder<T> {
private conditions: Array<(item: T) => boolean> = [];

where(predicate: (item: T) => boolean): this {
this.conditions.push(predicate);
return this;
}

execute(items: T[]): T[] {
return items.filter(item =>
this.conditions.every(condition => condition(item))
);
}
}

interface User {
name: string;
age: number;
}

const users: User[] = [
{ name: "Alice", age: 25 },
{ name: "Bob", age: 30 }
];

const result = new QueryBuilder<User>()
.where(u => u.age > 20)
.where(u => u.name.startsWith("A"))
.execute(users);

11. Higher-Order Generics

type Container<T> = {
value: T;
};

type Transform<F, T> = F extends (value: infer U) => infer R
? (container: Container<U>) => Container<R>
: never;

type MapString = Transform<(x: string) => number, string>;
// (container: Container<string>) => Container<number>

12. Generic Pipeline

class Pipeline<T> {
constructor(private value: T) {}

pipe<U>(fn: (value: T) => U): Pipeline<U> {
return new Pipeline(fn(this.value));
}

get(): T {
return this.value;
}
}

const result = new Pipeline(5)
.pipe(x => x * 2)
.pipe(x => x.toString())
.pipe(x => x + "!")
.get(); // "10!"

Key Takeaways

Constraints limit generic types
keyof enables type-safe property access
Default parameters provide fallback types
Recursive generics handle nested structures
Variance affects type compatibility
✅ Builder and pipeline patterns with generics


Practice Exercises

Exercise 1: Generic Cache

class Cache<K, V> {
private store = new Map<K, V>();

set(key: K, value: V): void {
this.store.set(key, value);
}

get(key: K): V | undefined {
return this.store.get(key);
}
}

Exercise 2: Generic Event Emitter

Create a type-safe event emitter with generic event types.


Next Steps

In Module 21, we'll explore Type Inference Deep Dive to understand how TypeScript infers types automatically.