Generics
TypeScript Programming
Generics
Generics allow you to write functions, classes, and interfaces that work with any type while preserving type safety. Instead of using any (which throws away type information) or writing separate functions for each type, generics let you parameterize types. The actual type is determined when the function is called or the class is instantiated, and TypeScript tracks it throughout the scope. Generics are one of TypeScript's most important features for building reusable libraries and frameworks.
Generic Functions
A generic function declares one or more type parameters in angle brackets before the parameter list. The type parameter acts as a placeholder that gets filled in when the function is called.
// Generic identity function
function identity<T>(value: T): T {
return value;
}
// TypeScript infers T from the argument
const str = identity("hello"); // T is string, returns string
const num = identity(42); // T is number, returns number
const arr = identity([1, 2, 3]); // T is number[], returns number[]
// Explicit type argument
const explicit = identity<boolean>(true);
// Generic function with multiple type parameters
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
const result = pair("age", 30); // [string, number]
const coords = pair(10.5, 20.3); // [number, number]
Generic Interfaces and Type Aliases
Generics work with interfaces and type aliases to create reusable type templates for data structures.
// Generic interface for API responses
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: string;
}
interface User {
id: number;
name: string;
}
// Specialize the generic with a concrete type
const userResponse: ApiResponse<User> = {
data: { id: 1, name: "Alice" },
status: 200,
message: "OK",
timestamp: new Date().toISOString(),
};
const listResponse: ApiResponse<User[]> = {
data: [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }],
status: 200,
message: "OK",
timestamp: new Date().toISOString(),
};
Generic Classes
Classes can also be parameterized with generics. This is commonly used for data structures like stacks, queues, and repositories.
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;
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
const numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
console.log(numberStack.pop()); // 20 (type: number | undefined)
const stringStack = new Stack<string>();
stringStack.push("hello");
// stringStack.push(42); // Error: number is not assignable to string
Generic Constraints
Constraints limit the types that can be used as generic arguments using the extends keyword. This ensures the generic type has certain properties or capabilities.
// Constraint: T must have a 'length' property
function logLength<T extends { length: number }>(item: T): void {
console.log(`Length: ${item.length}`);
}
logLength("hello"); // OK — string has length
logLength([1, 2, 3]); // OK — array has length
// logLength(42); // Error — number has no length
// Constraint: key must be a key of the object
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = { name: "Alice", age: 30, email: "alice@test.com" };
const name = getProperty(person, "name"); // type: string
const age = getProperty(person, "age"); // type: number
// getProperty(person, "phone"); // Error: "phone" is not a key of person
// Multiple constraints with intersection
interface HasId { id: number; }
interface HasName { name: string; }
function display<T extends HasId & HasName>(item: T): string {
return `#${item.id}: ${item.name}`;
}
Tip: Name generic type parameters with descriptive single letters by convention:Tfor a general type,Kfor keys,Vfor values,Efor elements. For complex generics with multiple parameters, consider longer names likeTInputandTOutput.
Key Takeaways
- Generics parameterize types, enabling reusable code that preserves type safety.
- TypeScript can infer generic type arguments from function call arguments.
- Generic interfaces and classes create flexible, reusable data structure templates.
- Use
extendsto constrain generic types to those with required properties. - The
keyofoperator combined with generics enables type-safe property access.