Magento 2 Experten — Hyvä Theme, Tailwind CSS & SEO aus einer Hand ›

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

src/models/HasIsbn.ts
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 HasIsbn

T 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 book

K 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

src/repository/Repository.ts
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.