Module 6: Generics
Generics allow you to create reusable components that work with multiple types while maintaining type safety.
1. Why Generics?
Without Generics
function identityNumber(value: number): number {
return value;
}
function identityString(value: string): string {
return value;
}
With Generics
function identity<T>(value: T): T {
return value;
}
let num = identity<number>(42);
let str = identity<string>("Hello");
let bool = identity<boolean>(true);
Type Inference
TypeScript can often infer the generic type:
let value = identity(100); // Type inferred as number
2. Generic Functions
Basic Generic Function
function getFirst<T>(arr: T[]): T | undefined {
return arr[0];
}
let firstNumber = getFirst([1, 2, 3]); // number | undefined
let firstName = getFirst(["a", "b", "c"]); // string | undefined
Multiple Type Parameters
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
let result1 = pair<string, number>("age", 25);
let result2 = pair("Alice", true); // Type inferred
3. Generic Interfaces
interface Box<T> {
value: T;
}
let numberBox: Box<number> = { value: 42 };
let stringBox: Box<string> = { value: "Hello" };
let boolBox: Box<boolean> = { value: true };
Generic Interface with Methods
interface Container<T> {
value: T;
getValue(): T;
setValue(value: T): void;
}
class NumberContainer implements Container<number> {
constructor(public value: number) {}
getValue(): number {
return this.value;
}
setValue(value: number): void {
this.value = value;
}
}
4. Generic Classes
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
let numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 2
let stringStack = new Stack<string>();
stringStack.push("Hello");
stringStack.push("World");
console.log(stringStack.peek()); // "World"
5. Generic Constraints
Restrict generic types using extends.
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(value: T): void {
console.log(`Length: ${value.length}`);
}
logLength("Hello"); // ✅ string has length
logLength([1, 2, 3]); // ✅ array has length
logLength({ length: 10 }); // ✅ object with length
// logLength(42); // ❌ Error: number doesn't have length
Using Type Parameters in Constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
let person = { name: "Alice", age: 25 };
let name = getProperty(person, "name"); // "Alice"
let age = getProperty(person, "age"); // 25
// getProperty(person, "salary"); // ❌ Error: 'salary' doesn't exist
6. Generic Type Aliases
type Nullable<T> = T | null;
type ResponseData<T> = {
data: T;
status: number;
message: string;
};
let userName: Nullable<string> = "Alice";
userName = null; // ✅ OK
let userResponse: ResponseData<{ id: number; name: string }> = {
data: { id: 1, name: "Alice" },
status: 200,
message: "Success"
};
7. Default Generic Parameters
interface APIResponse<T = any> {
data: T;
status: number;
}
let response1: APIResponse = {
data: "Hello",
status: 200
}; // Default to any
let response2: APIResponse<string[]> = {
data: ["a", "b", "c"],
status: 200
};
8. Generic Utility Functions
Array Utilities
function reverse<T>(arr: T[]): T[] {
return arr.reverse();
}
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}
let numbers = [1, 2, 3, 4, 5];
let doubled = map(numbers, n => n * 2); // [2, 4, 6, 8, 10]
Object Utilities
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
let result = merge(
{ name: "Alice" },
{ age: 25 }
);
// result has type { name: string } & { age: number }
9. Generic Conditional Types
type IsArray<T> = T extends any[] ? true : false;
type Test1 = IsArray<number[]>; // true
type Test2 = IsArray<string>; // false
10. Real-World Example: Repository Pattern
interface Entity {
id: number;
}
class Repository<T extends Entity> {
private items: T[] = [];
create(item: T): void {
this.items.push(item);
}
findById(id: number): T | undefined {
return this.items.find(item => item.id === id);
}
findAll(): T[] {
return this.items;
}
delete(id: number): boolean {
const index = this.items.findIndex(item => item.id === id);
if (index !== -1) {
this.items.splice(index, 1);
return true;
}
return false;
}
}
interface User extends Entity {
name: string;
email: string;
}
interface Product extends Entity {
name: string;
price: number;
}
let userRepo = new Repository<User>();
userRepo.create({ id: 1, name: "Alice", email: "alice@example.com" });
let productRepo = new Repository<Product>();
productRepo.create({ id: 1, name: "Laptop", price: 999 });
11. Generic Promise Patterns
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
const data = await response.json();
return data as T;
}
interface User {
id: number;
name: string;
}
// Usage
const user = await fetchData<User>("/api/user/1");
console.log(user.name);
Key Takeaways
✅ Generics create reusable, type-safe components
✅ Use constraints to restrict generic types
✅ keyof enables type-safe property access
✅ Generics work with functions, interfaces, and classes
✅ Default parameters provide fallback types
✅ Essential for building flexible libraries
Practice Exercises
Exercise 1: Generic Queue
class Queue<T> {
private items: T[] = [];
enqueue(item: T): void {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
size(): number {
return this.items.length;
}
}
Exercise 2: Generic Filter Function
function filter<T>(arr: T[], predicate: (item: T) => boolean): T[] {
return arr.filter(predicate);
}
let numbers = [1, 2, 3, 4, 5];
let evenNumbers = filter(numbers, n => n % 2 === 0);
Exercise 3: Generic Cache
Create a generic cache class that stores key-value pairs.
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);
}
has(key: K): boolean {
return this.store.has(key);
}
}
Next Steps
In Module 7, we'll explore Union and Intersection Types to create complex type combinations.