Generics with Constraints in TypeScript
Generics with Constraints
~15 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
Our Repository<T> from chapter 17 works for ANY type – but some operations (e.g. "find by ISBN") only make sense if T is GUARANTEED to have an isbn property. Constraints restrict WHICH types are even allowed as T.
The problem without a constraint
export class Repository<T> {
protected items: T[] = [];
findByIsbn(isbn: string): T | undefined {
return this.items.find((item) => item.isbn === isbn); // error!
// TypeScript doesn't know T even HAS an 'isbn' property -
// T could theoretically be 'string' or 'number', with NO properties at all
}
}The fix: extends as a constraint
export interface HasIsbn {
isbn: string;
}import { HasIsbn } from '../models/HasIsbn.js';
export class Repository<T extends HasIsbn> {
protected items: T[] = [];
findByIsbn(isbn: string): T | undefined {
return this.items.find((item) => item.isbn === isbn); // ✓ now allowed!
}
}
new Repository<Book>(); // ✓ Book HAS an isbn property
new Repository<string>(); // ✗ error - string doesn't satisfy HasIsbnT extends HasIsbn means: "T can be ANY type, AS LONG AS it satisfies AT LEAST the shape of HasIsbn" – T can still have FURTHER properties (our Book has far more than just isbn), but must have AT LEAST isbn: string.
Combining multiple requirements
interface HasStatus {
status: string;
}
function showAvailableItems<T extends HasIsbn & HasStatus>(items: T[]): T[] {
return items.filter((item) => item.status === 'available');
}T extends HasIsbn & HasStatus combines intersection types (chapter 9) with generic constraints: T must satisfy BOTH interfaces at once.
Bonus: keyof for type-safe property access
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const book = { title: 'Test', pageCount: 300 };
getProperty(book, 'title'); // ✓ return type: string
getProperty(book, 'pageCount'); // ✓ return type: number
getProperty(book, 'author'); // ✗ error - 'author' doesn't exist on bookK extends keyof T is an ADVANCED, but extremely useful pattern: keyof T is a union of ALL of T's property names as string literal types (here: 'title' | 'pageCount') – K MUST be one of them, and the return type T[K] AUTOMATICALLY adapts to whichever key was passed.
In practice: consistently using Repository with a constraint
import { HasIsbn } from '../models/HasIsbn.js';
export class Repository<T extends HasIsbn> {
protected items: T[];
constructor(initialItems: T[] = []) {
this.items = initialItems;
}
all(): readonly T[] {
return this.items;
}
add(item: T): void {
this.items.push(item);
}
count(): number {
return this.items.length;
}
find(predicate: (item: T) => boolean): T | undefined {
return this.items.find(predicate);
}
findByIsbn(isbn: string): T | undefined {
return this.items.find((item) => item.isbn === isbn);
}
}Book and Audiobook (chapter 13) both satisfy HasIsbn automatically, since Medium already provides the isbn property – NO changes needed to the existing model classes.
Tipp: Rule of thumb: add a constraint (extends) AS SOON AS you need to access a SPECIFIC property of T inside a generic function/class. Without accessing specific properties (like our firstElement<T> from chapter 17), NO constraint is needed at all – the FEWER restrictions, the MORE reusable the function.