Generic интерфейсы и классы

Generic интерфейсы и классы — конструкции TypeScript, параметризованные типовыми переменными, которые позволяют описывать универсальные контракты и реализации, сохраняя строгую типизацию при разных типах данных.

Зачем нужно

Generic-параметры на уровне интерфейса или класса позволяют описать единый контракт (например, репозиторий, коллекцию, обёртку) и переиспользовать его с разными типами данных без копирования кода. Это основа для паттернов Repository, Service, Builder, Observable и других.

Где используется

  • Репозитории и сервисы: Repository<User>, Service<Order>
  • Коллекции и контейнеры: Stack<T>, Queue<T>, Cache<K, V>
  • Обёртки результатов: ApiResponse<T>, Paginated<T>
  • Паттерн Builder, Factory
  • React-компоненты с generic props

Основной контент

Generic интерфейс

interface Repository<T> {
  findById(id: string): Promise<T | null>;
  findAll: Promise<T>;
  save(entity: T): Promise<T>;
  delete(id: string): Promise<void>;
}

interface User {
  id: string;
  name: string;
  email: string;
}

class UserRepository implements Repository<User> {
  async findById(id: string): Promise<User | null> {
    // реализация
    return null;
  }
  async findAll: Promise<User> { return ; }
  async save(user: User): Promise<User> { return user; }
  async delete(id: string): Promise<void> {}
}

Generic интерфейс с несколькими параметрами

interface Cache<K, V> {
  get(key: K): V | undefined;
  set(key: K, value: V): void;
  has(key: K): boolean;
  delete(key: K): boolean;
}

class MapCache<K, V> implements Cache<K, V> {
  private store = new Map<K, V>;
  get(key: K) { return this.store.get(key); }
  set(key: K, value: V) { this.store.set(key, value); }
  has(key: K) { return this.store.has(key); }
  delete(key: K) { return this.store.delete(key); }
}

const cache = new MapCache<string, number>;
cache.set("count", 42);
const val = cache.get("count"); // number | undefined

Generic класс

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];
  }

  get size: number {
    return this.items.length;
  }
}

const numbers = new Stack<number>;
numbers.push(1);
numbers.push(2);
const top = numbers.pop(); // number | undefined

const words = new Stack<string>;
words.push("hello");

Ограничения в generic классах

interface Identifiable {
  id: string;
}

class EntityService<T extends Identifiable> {
  private entities = new Map<string, T>;

  add(entity: T): void {
    this.entities.set(entity.id, entity); // OK — T имеет id
  }

  findById(id: string): T | undefined {
    return this.entities.get(id);
  }
}

const userService = new EntityService<User>;
userService.add({ id: "1", name: "Alice", email: "a@b.com" });

Generic интерфейс с callable/index signature

interface Transformer<TIn, TOut> {
  (input: TIn): TOut;
}

interface Collection<T> {
  [index: number]: T;
  length: number;
}

Частые ошибки

  • Не указывать тип при создании экземпляраnew Stack без параметра даст Stack<unknown>, лучше явно new Stack<number>.
  • Смешивать generic-параметр класса и метода — параметр класса T доступен во всех методах; не переиспользуйте то же имя в параметре метода.
  • Реализовывать generic интерфейс с конкретным типом не полностью — все методы должны быть реализованы с правильными сигнатурами.
  • Использовать generic там, где достаточно unknown — если тип не важен и не используется в нескольких местах — generic избыточен.

Связанные темы

Ресурсы