Hint
Extend from an interface or inline shape — ensures the generic value has the properties you need
Combining generics with interface constraints lets you write functions that accept any compatible type while retaining full type information.
// Constraint via interface
interface HasId {
id: number | string;
}
function findById<T extends HasId>(items: T[], id: T['id']): T | undefined {
return items.find(item => item.id === id);
}
// Works with any object that has an id field
const user = findById(users, 1); // T = User
const post = findById(posts, 'abc'); // T = Post
// Return type is User | undefined and Post | undefined respectively
// Multiple interface constraints (intersection)
interface Serializable { serialize(): string }
interface Comparable<T> { compareTo(other: T): number }
function sortAndSerialize<T extends Serializable & Comparable<T>>(items: T[]): string[] {
return items
.sort((a, b) => a.compareTo(b))
.map(item => item.serialize());
}
// Constraint + keyof for safe property access
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
return items.map(item => item[key]);
}
const names = pluck(users, 'name'); // string[]
const ids = pluck(users, 'id'); // number[]
T extends HasId is more flexible than accepting HasId[] directly — it preserves the full type of the items, so the return type is T | undefined not just HasId | undefined.