$ sudo teach IT
МОДУЛЬ 6 · УРОК 3

Обобщённые интерфейсы и классы

Интерфейсы и классы с generics: создание обобщённых структур данных

Цели урока

  • Научиться создавать обобщённые интерфейсы
  • Понять, как работают обобщённые классы
  • Реализовать простые коллекции с generics
  • Понять связь между интерфейсами и классами

Обобщённые интерфейсы

Интерфейсы с generics позволяют описывать структуру данных, которая может работать с разными типами. Это похоже на обобщённые функции, но описывает структуру объекта вместо поведения:

// Обобщённый интерфейс для пары значений
interface Pair<T, U> {
  first: T;
  second: U;
}

// Использование с разными типами:
const pair1: Pair<string, number> = {
  first: "возраст",
  second: 25
};

const pair2: Pair<boolean, string[]> = {
  first: true,
  second: ["привет", "мир"]
};

Интерфейс Pair описывает объект с двумя свойствами, но типы этих свойств определяются при использовании.

Обобщённые интерфейсы с ограничениями

Вы можете ограничить типы в интерфейсе, чтобы они соответствовали определённой структуре:

// T должен иметь id
interface Identifiable<T extends { id: number }> {
  data: T;
  findById(id: number): T | undefined;
}

// Тип с id — работает:
interface User { id: number; name: string; }
// Identifiable — ОК!

// Тип без id — ошибка:
// Identifiable — Ошибка!

Обобщённые классы

Классы с generics — это один из самых мощных инструментов TypeScript. Они позволяют создавать контейнеры и коллекции для данных любого типа:

// Обобщённый класс-контейнер
class Container<T> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  get(index: number): T | undefined {
    return this.items[index];
  }

  getAll(): T[] {
    return [...this.items];
  }

  find(predicate: (item: T) => boolean): T | undefined {
    return this.items.find(predicate);
  }
}

// Использование:
const numbers = new Container<number>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
numbers.get(0); // 1 (number)

const strings = new Container<string>();
strings.add("привет");
strings.add("мир");
strings.get(0); // "привет" (string)

Класс Container работает с любым типом данных, сохраняя при этом типобезопасность. Методы get и find возвращают элементы правильного типа.

Практический пример: стек (Stack)

Реализуем классический структуру данных — стек, который работает с элементами любого типа:

// Обобщённый класс Stack
class Stack<T> {
  private elements: T[] = [];

  push(item: T): void {
    this.elements.push(item);
  }

  pop(): T | undefined {
    return this.elements.pop();
  }

  peek(): T | undefined {
    return this.elements[this.elements.length - 1];
  }

  isEmpty(): boolean {
    return this.elements.length === 0;
  }

  size(): number {
    return this.elements.length;
  }
}

// Стек чисел:
const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
numStack.push(3);
numStack.pop(); // 3 (number)
numStack.peek(); // 2 (number)

// Стек строк:
const strStack = new Stack<string>();
strStack.push("привет");
strStack.push("мир");
strStack.pop(); // "мир" (string)

Практический пример: очередь (Queue)

Ещё одна классическая структура данных — очередь (FIFO: First In, First Out):

// Обобщённый класс Queue
class Queue<T> {
  private items: T[] = [];

  enqueue(item: T): void {
    this.items.push(item);
  }

  dequeue(): T | undefined {
    return this.items.shift();
  }

  front(): T | undefined {
    return this.items[0];
  }

  isEmpty(): boolean {
    return this.items.length === 0;
  }

  size(): number {
    return this.items.length;
  }
}

// Очередь задач:
const taskQueue = new Queue<string>();
taskQueue.enqueue("Задача 1");
taskQueue.enqueue("Задача 2");
taskQueue.enqueue("Задача 3");
taskQueue.dequeue(); // "Задача 1" (первая вошла — первая вышла)
taskQueue.front(); // "Задача 2"

Обобщённые интерфейсы для API

Обобщённые интерфейсы широко используются для типизации ответов API:

// Обобщённые интерфейсы для API
interface ApiResponse<T> {
  status: "success" | "error";
  data: T | null;
  error?: string;
  timestamp: number;
}

// Обобщённый интерфейс для пагинации
interface PaginatedResponse<T> {
  items: T[];
  total: number;
  page: number;
  pageSize: number;
  hasMore: boolean;
}

// Использование:
interface User { id: number; name: string; }

const response: ApiResponse<PaginatedResponse<User>> = {
  status: "success",
  data: {
    items: [{ id: 1, name: "Алиса" }],
    total: 100,
    page: 1,
    pageSize: 10,
    hasMore: true
  },
  timestamp: Date.now()
};

Обобщённые классы с интерфейсами

Классы могут реализовывать обобщённые интерфейсы:

// Интерфейс для хранилища
interface Store<T> {
  add(item: T): void;
  get(id: number): T | undefined;
  getAll(): T[];
  remove(id: number): boolean;
}

// Класс, реализующий интерфейс
class MemoryStore<T extends { id: number }> implements Store<T> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  get(id: number): T | undefined {
    return this.items.find(item => item.id === id);
  }

  getAll(): T[] {
    return [...this.items];
  }

  remove(id: number): boolean {
    const index = this.items.findIndex(item => item.id === id);
    if (index !== -1) {
      this.items.splice(index, 1);
      return true;
    }
    return false;
  }
}

// Использование:
interface User { id: number; name: string; }
const userStore = new MemoryStore<User>();
userStore.add({ id: 1, name: "Алиса" });
userStore.get(1); // { id: 1, name: "Алиса" }

Наследование обобщённых классов

Обобщённые классы могут наследоваться друг от друга:

// Базовый обобщённый класс
class BaseCollection<T> {
  protected items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  count(): number {
    return this.items.length;
  }
}

// Дочерний класс с дополнительным параметром типа
class SearchableCollection<T, K extends keyof T> extends BaseCollection<T> {
  search(key: K, value: T[K]): T | undefined {
    return this.items.find(item => item[key] === value);
  }
}

// Использование:
const users = new SearchableCollection<User, "name" | "age">();
users.add({ name: "Алиса", age: 25 });
users.search("name", "Алиса"); // OK!
// users.search("email", "a@b.com"); // Ошибка!

Обобщённые интерфейсы для функций

Интерфейсы могут описывать функции с generics:

// Интерфейс для функции сравнения
interface Comparator<T> {
  (a: T, b: T): number;
}

// Интерфейс для функции-обработчика
interface EventHandler<T> {
  (event: T): void;
}

// Использование:
const numberComparator: Comparator<number> = (a, b) => a - b;
const stringComparator: Comparator<string> = (a, b) => a.localeCompare(b);

// Использование в функции сортировки:
function sortWith<T>(arr: T[], comparator: Comparator<T>): T[] {
  return [...arr].sort(comparator);
}

sortWith([3, 1, 2], numberComparator); // [1, 2, 3]

Советы по использованию

Используйте обобщённые классы для:
  • Коллекций (списки, очереди, стеки)
  • Хранилищ данных (репозитории)
  • Контейнеров (коробки, пакеты)
  • Кэшей и буферов
Используйте обобщённые интерфейсы для:
  • Описания структуры API-ответов
  • Типизации функций-обработчиков
  • Описания контрактов между компонентами
  • Создания собственных утилитарных типов

Итоги урока

  • Обобщённые интерфейсы описывают структуру данных с параметрами типа
  • Обобщённые классы создают контейнеры и коллекции для данных любого типа
  • Классы могут реализовывать обобщённые интерфейсы
  • Обобщённые классы могут наследоваться и расширяться
  • Generics позволяют создавать типобезопасные структуры данных
  • Интерфейсы могут описывать функции с generics
  • Используйте generics для повторяющихся паттернов

Практический пример: типизированный Map

Создадим обобщённый класс Map, который хранит пары ключ-значение:

// Обобщённый класс TypedMap
class TypedMap<K extends string | number, V> {
  private entries: Map<K, V> = new Map();

  set(key: K, value: V): void {
    this.entries.set(key, value);
  }

  get(key: K): V | undefined {
    return this.entries.get(key);
  }

  has(key: K): boolean {
    return this.entries.has(key);
  }

  delete(key: K): boolean {
    return this.entries.delete(key);
  }

  values(): V[] {
    return Array.from(this.entries.values());
  }
}

// Использование:
const scores = new TypedMap<string, number>();
scores.set("Алиса", 95);
scores.set("Борис", 87);
scores.get("Алиса"); // 95 (number)

Практический пример: типизированный Event Emitter

Создадим обобщённый класс для работы с событиями:

// Обобщённый класс для событий
class EventEmitter<T extends Record<string, any>> {
  private listeners: Map<keyof T, Function[]> = new Map();

  on<K extends keyof T>(
    event: K,
    callback: (data: T[K]) => void
  ): void {
    const fns = this.listeners.get(event) || [];
    fns.push(callback);
    this.listeners.set(event, fns);
  }

  emit<K extends keyof T>(
    event: K,
    data: T[K]
  ): void {
    // Вызываем обработчики
  }
}

// Использование:
const emitter = new EventEmitter<{
  click: { x: number; y: number };
  keyPress: { key: string };
}>();

emitter.on("click", (data) => {
  console.log(data.x, data.y); // OK!
});

Следующий урок

Ограничения generics (extends) — разберём, как ограничивать типы с помощью extends и keyof

Тест по обобщённым интерфейсам и классам

8 вопросов

Обобщённый ответ API

Premium