Generics scale beyond a single type parameter: multiple parameters can reference each other (one constrained by keyof another), a type parameter can have a default so most callers never need to specify it explicitly, and a whole class can be generic, applying its type parameter consistently to every property and method inside it.
Multiple type parameters referencing each other
K is constrained to T's own keys, so TypeScript rejects a call passing a property name that doesn't actually exist on the object being updated.
function updateProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): T {
return { ...obj, [key]: value };
}
const user = { name: "Alice", age: 30 };
updateProperty(user, "age", 31); // fine
// updateProperty(user, "age", "old"); // COMPILE ERROR: age must be a number
// updateProperty(user, "email", "x"); // COMPILE ERROR: email isn't a key of userDefault type parameters
Most callers can omit the type parameter entirely and get a sensible default, while advanced callers can still override it — the same tradeoff a default function parameter makes.
interface ApiResponse<T = unknown> {
data: T;
status: number;
}
const generic: ApiResponse = { data: "anything", status: 200 }; // T defaults to unknown
const typed: ApiResponse<{ name: string }> = { data: { name: "Bob" }, status: 200 }; // T explicitly givenGeneric classes
The type parameter is fixed once at construction and applies consistently to every method and property in the class, giving a type-safe, reusable container.
class Box<T> {
private contents: T;
constructor(value: T) {
this.contents = value;
}
get(): T {
return this.contents;
}
set(value: T): void {
this.contents = value;
}
}
const stringBox = new Box("hello"); // Box<string>, inferred from the constructor argument
const num: number = new Box(42).get();