Generics let you write functions, classes and interfaces that work with any type while preserving full type safety. They are the foundation of reusable, type-safe code.
Without generics, you would use any (losing type safety) or write
duplicate code for every type.
The Problem Generics Solve
TypeScript
// ❌ Without generics — loses type information
function first(arr: any[]): any {
return arr[0];
}
const num = first([1, 2, 3]); // any — we lost the type!
const str = first(["a", "b"]); // any — same problem
// ✅ With generics — preserves type information
function first<T>(arr: T[]): T {
return arr[0];
}
const num = first([1, 2, 3]); // number — type preserved!
const str = first(["a", "b"]); // string — type preserved!Generic Functions
TypeScript
function identity<T>(value: T): T {
return value;
}
// TypeScript infers T from the argument
identity("hello"); // T = string
identity(42); // T = number
// Or specify explicitly
identity<string>("hello");Multiple Type Parameters
TypeScript
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
pair("hello", 42); // [string, number]
pair(1, true); // [number, boolean]Generic Interfaces
TypeScript
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// A user response
const userResponse: ApiResponse<{ name: string; age: number }> = {
data: { name: "Sita", age: 21 },
status: 200,
message: "OK",
};
// A list response
const listResponse: ApiResponse<string[]> = {
data: ["Kathmandu", "Pokhara"],
status: 200,
message: "OK",
};Generic Classes
TypeScript
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];
}
get size(): number {
return this.items.length;
}
}
const numbers = new Stack<number>();
numbers.push(1);
numbers.push(2);
numbers.pop(); // 2 — typed as number
const strings = new Stack<string>();
strings.push("hello");
strings.push("world");Generic Constraints
Use extends to require that a type has certain properties:
TypeScript
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): void {
console.log(`Length: ${item.length}`);
}
logLength("hello"); // 5
logLength([1, 2, 3]); // 3
logLength({ length: 10, name: "test" }); // 10
// logLength(42); // Error: number doesn't have .lengthThe keyof Constraint
TypeScript
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Sita", age: 21 };
getProperty(user, "name"); // string — type-safe
getProperty(user, "age"); // number — type-safe
// getProperty(user, "email"); // Error: "email" is not in keyof UserBuilt-in Utility Types
TypeScript ships with powerful generic utility types:
| Utility | What it does | Example |
|---|---|---|
Partial<T> |
All properties optional | Partial<User> |
Required<T> |
All properties required | Required<User> |
Readonly<T> |
All properties readonly | Readonly<User> |
Pick<T, K> |
Only specified properties | Pick<User, "name" | "age"> |
Omit<T, K> |
All except specified properties | Omit<User, "password"> |
Record<K, V> |
Object with keys K and values V | Record<string, number> |
Exclude<T, U> |
Remove types from a union | Exclude<"a" | "b", "a"> |
ReturnType<F> |
Get return type of a function | ReturnType<typeof myFunc> |
TypeScript
interface User {
name: string;
age: number;
email: string;
password: string;
}
type PublicUser = Omit<User, "password">;
type UserUpdate = Partial<Pick<User, "name" | "email">>;Tips
- Use generics, not
any— generics preserve type information. - Name type parameters with single capital letters:
T,K,V, or descriptive names likeTItem,TResponse. - Use constraints (
extends) to prevent misuse. - Start simple — add generic parameters only when you need them.
Next: modules — organising code across files.