05 - Generics
What are Generics?
Generics let you write reusable code that works with any type, while still keeping type safety. Instead of using any, you use a type variable (like T) that gets filled in when you use the function/class.
Basic Generic Function
// Without generics — loses type info
function identity(arg: any): any {
return arg; // returns any — TypeScript forgets the type
}
// With generics — preserves type info
function identity<T>(arg: T): T {
return arg; // returns the same type that was passed in
}
const num = identity<number>(42); // T = number, returns number
const str = identity("hello"); // T inferred as string, returns string<T> is a placeholder — it gets replaced with the actual type when you call the function.
Generic Interfaces
interface Box<T> {
value: T;
}
const numberBox: Box<number> = { value: 123 };
const stringBox: Box<string> = { value: "hello" };Generic Classes
class DataStore<T> {
private data: T[] = [];
add(item: T): void {
this.data.push(item);
}
get(index: number): T | undefined {
return this.data[index];
}
getAll(): T[] {
return [...this.data];
}
}
const users = new DataStore<string>();
users.add("Alice");
users.add(42); // ❌ error — only strings allowedGeneric Constraints
Limit what types T can be using extends:
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(arg: T): void {
console.log(arg.length);
}
logLength("hello"); // ✅ string has .length
logLength([1, 2, 3]); // ✅ array has .length
logLength(123); // ❌ error — number doesn't have .lengthThe keyof Operator
keyof takes an object type and produces a union of its keys:
interface User {
id: number;
name: string;
email: string;
}
type UserKeys = keyof User; // "id" | "name" | "email"It's especially useful with generics to create type-safe property access:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Alice", email: "a@b.com" };
getProperty(user, "name"); // ✅ returns string
getProperty(user, "age"); // ❌ error — "age" is not a key of UserK extends keyof T means K can only be one of T's actual keys — TypeScript catches typos and invalid keys at compile time.
Multiple Type Parameters
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const result = pair("age", 30); // [string, number]Real-World Example: API Response
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
interface User {
id: number;
name: string;
}
interface Post {
id: number;
title: string;
}
// Same response shape, different data types
const userResponse: ApiResponse<User> = {
data: { id: 1, name: "Alice" },
status: 200,
message: "OK"
};
const postResponse: ApiResponse<Post> = {
data: { id: 1, title: "Hello World" },
status: 200,
message: "OK"
};Key Takeaways
- Generics = reusable code with type safety (instead of
any) <T>is a type variable — gets replaced with the actual typeT extends Somethingconstrains what types are allowed- Common in APIs, data stores, and utility functions
- TypeScript often infers the type — you don't always need
<number>explicitly