Generics allow you to create reusable components that work with multiple types.
Why Generics?
Without generics, you'd need to use any or create separate functions:
// ❌ Using any - loses type safety
function identity(arg: any): any {
return arg;
}
// ❌ Creating separate functions - not reusable
function identityString(arg: string): string {
return arg;
}
function identityNumber(arg: number): number {
return arg;
}
With generics:
// ✅ Generic function - type-safe and reusable
function identity<T>(arg: T): T {
return arg;
}
identity<string>("hello"); // string
identity<number>(42); // number
identity("hello"); // TypeScript infers T as string
Generic Functions
Basic Generic Function
function getFirst<T>(items: T[]): T | undefined {
return items[0];
}
getFirst<string>(["a", "b", "c"]); // string | undefined
getFirst<number>([1, 2, 3]); // number | undefined
getFirst([true, false]); // TypeScript infers boolean
Multiple Type Parameters
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
pair<string, number>("Alice", 25); // [string, number]
pair<boolean, string>(true, "yes"); // [boolean, string]
Generic Constraints
Limit what types can be used.
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
Using Type Parameters in Constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = { name: "Alice", age: 25 };
getProperty(person, "name"); // string
getProperty(person, "age"); // number
// getProperty(person, "email"); // ❌ Error: "email" is not a key
Generic Interfaces
interface Box<T> {
value: T;
}
const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };
Generic Interface with Methods
interface Repository<T> {
findById(id: string): T | undefined;
save(entity: T): void;
delete(id: string): void;
}
class UserRepository implements Repository<User> {
findById(id: string): User | undefined {
// Implementation
return undefined;
}
save(entity: User): void {
// Implementation
}
delete(id: string): void {
// Implementation
}
}
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];
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
const stringStack = new Stack<string>();
stringStack.push("hello");
Generic Type Aliases
type Result<T> = {
success: boolean;
data?: T;
error?: string;
};
type ApiResponse<T> = Promise<Result<T>>;
Default Type Parameters
Provide default types for generics.
interface ApiResponse<T = any> {
data: T;
status: number;
}
const response1: ApiResponse = { data: "anything", status: 200 };
const response2: ApiResponse<User> = { data: user, status: 200 };
Exercises
Exercise 1: Basic Generic Function
// Create a generic function 'swap' that:
// - Takes two parameters of the same type
// - Returns a tuple with them swapped
// Example: swap(1, 2) returns [2, 1]
Exercise 2: Generic Array Functions
// Create generic functions:
// - 'last': returns the last element of an array
// - 'reverse': returns a reversed array
// - 'unique': returns array with duplicates removed
Exercise 3: Generic Constraints
// Create a generic function 'compare' that:
// - Takes two parameters that extend { id: number }
// - Returns true if they have the same id
Exercise 4: Generic Class
// Create a generic class 'Queue' with:
// - enqueue(item: T): void
// - dequeue(): T | undefined
// - isEmpty(): boolean
// - size(): number
Exercise 5: Generic Interface
// Create a generic interface 'Cache' with:
// - get(key: string): T | undefined
// - set(key: string, value: T): void
// - clear(): void
// Then implement it in a class
Best Practices
- Use descriptive type parameter names -
T,U,Vfor simple cases,TKey,TValuefor complex - Add constraints when needed - Use
extendsto limit types - Let TypeScript infer when possible - Don't always need explicit type arguments
- Use default types - When a sensible default exists
- Document complex generics - Add comments explaining constraints
Common Mistakes
- Overusing generics
// ❌ Unnecessary generic
function process<T>(value: string): string {
return value.toUpperCase();
}
// ✅ Simpler
function process(value: string): string {
return value.toUpperCase();
}
- Not using constraints
// ❌ Error-prone
function getLength<T>(item: T): number {
return item.length; // Error: Property 'length' doesn't exist
}
// ✅ With constraint
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
- Forgetting type inference
// ❌ Verbose
const result = identity<string>("hello");
// ✅ Let TypeScript infer
const result = identity("hello");
Next Steps
Learn about Utility Types.