Generics are one of TypeScript's most powerful features. Beyond basic generics, you can use constraints, multiple type parameters, and generic utility types to create highly flexible and type-safe code.
Constraints limit what types can be used as generic parameters using
the extends keyword:
// Constraint: T must have a length property
function logLength<T extends { length: number }>(item: T): void {
console.log(`Length: ${item.length}`);
}
logLength("hello"); // ✓ OK - strings have length
logLength([1, 2, 3]); // ✓ OK - arrays have length
// logLength(123); // ✗ Error - numbers don't have length
// Constraint with interface
interface Nameable {
name: string;
}
function greetEntity<T extends Nameable>(entity: T): string {
return `Hello, ${entity.name}!`;
}
greetEntity({ name: "Hero", level: 5 }); // ✓ OK
// greetEntity({ id: 1 }); // ✗ Error - missing 'name'
Functions and classes can have multiple generic type parameters:
// Function with two type parameters
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
const entry1 = pair("name", "Alice"); // [string, string]
const entry2 = pair(1, { data: "value" }); // [number, { data: string }]
// Generic map function
function mapObject<T, U>(
obj: Record<string, T>,
mapper: (value: T) => U
): Record<string, U> {
const result: Record<string, U> = {};
for (const key in obj) {
result[key] = mapper(obj[key]);
}
return result;
}
const numbers = { a: 1, b: 2, c: 3 };
const doubled = mapObject(numbers, n => n * 2);
// { a: 2, b: 4, c: 6 }
Classes can be generic, allowing flexible data structures:
// Generic Stack data structure
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;
}
size(): number {
return this.items.length;
}
}
// Use with different types
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 2
const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.push("world");
The keyof operator creates a union of an object's keys:
// Get property value by key name (type-safe)
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const player = {
name: "Hero",
level: 5,
health: 100
};
const name = getProperty(player, "name"); // string
const level = getProperty(player, "level"); // number
// const invalid = getProperty(player, "invalid"); // ✗ Error
// Update property (type-safe setter)
function setProperty<T, K extends keyof T>(
obj: T,
key: K,
value: T[K]
): void {
obj[key] = value;
}
setProperty(player, "level", 6); // ✓ OK
// setProperty(player, "level", "six"); // ✗ Error - wrong type
TypeScript can infer complex generic types from usage:
// TypeScript infers both type parameters
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const result = merge(
{ name: "Hero" },
{ level: 5 }
);
// Type inferred as: { name: string } & { level: number }
// Which simplifies to: { name: string; level: number }
console.log(result.name); // ✓ OK
console.log(result.level); // ✓ OK
// Conditional type with generics
type ArrayOrSingle<T> = T extends any[] ? T : T[];
function ensureArray<T>(value: T): ArrayOrSingle<T> {
return (Array.isArray(value) ? value : [value]) as ArrayOrSingle<T>;
}
const arr1 = ensureArray([1, 2, 3]); // number[]
const arr2 = ensureArray(5); // number[]
// Generic event system with type safety
type EventMap = {
"player:move": { x: number; y: number; };
"player:damage": { amount: number; source: string; };
"item:collect": { itemId: string; itemType: string; };
};
class EventBus<T extends Record<string, any>> {
private listeners: {
[K in keyof T]?: Array<(data: T[K]) => void>;
} = {};
on<K extends keyof T>(event: K, callback: (data: T[K]) => void): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(callback);
}
emit<K extends keyof T>(event: K, data: T[K]): void {
const callbacks = this.listeners[event];
if (callbacks) {
callbacks.forEach(callback => callback(data));
}
}
}
const events = new EventBus<EventMap>();
// Type-safe event listeners
events.on("player:move", (data) => {
console.log(`Player moved to ${data.x}, ${data.y}`);
});
events.on("player:damage", (data) => {
console.log(`Player took ${data.amount} damage from ${data.source}`);
});
// Type-safe event emission
events.emit("player:move", { x: 10, y: 5 });
events.emit("player:damage", { amount: 25, source: "enemy" });
// events.emit("player:move", { invalid: true }); // ✗ Error
extends to constrain generic typeskeyof T creates a union of object keysT[K] accesses the type of property K in T