Обобщённые интерфейсы и классы
Интерфейсы и классы с 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 описывает объект с двумя свойствами, но типы этих свойств определяются при использовании.
Обобщённые интерфейсы с ограничениями
Вы можете ограничить типы в интерфейсе, чтобы они соответствовали определённой структуре:
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)
Реализуем классический структуру данных — стек, который работает с элементами любого типа:
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):
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:
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, который хранит пары ключ-значение:
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 вопросов