Что такое дженерики (generics) и зачем они нужны?
В TypeScript дженерики (generics) являются мощным инструментом, который позволяет создавать обобщенные компоненты, работающие с разными типами данных, сохраняя строгую типизацию. Они дают возможность писать универсальный и при этом безопасный код, избегая дублирования и повышая гибкость архитектуры.
Основная идея дженериков
Идея дженериков заключается в том, чтобы не привязывать функцию, класс или интерфейс к конкретному типу заранее, а сделать их параметризованными. Вместо того чтобы указывать конкретный тип (например, string или number), разработчик использует переменную типа, например T.
Пример:
function identity<T>(value: T): T {
return value;
}
const num = identity<number>(10);
const str = identity<string>("Hello");
В данном примере функция identity принимает значение любого типа и возвращает его же, сохраняя типовую информацию.
Применение в функциях
Дженерики позволяют писать функции, которые работают с разными типами данных, сохраняя при этом строгую типизацию. Это удобно, когда нужно обрабатывать коллекции или данные без жесткой привязки к одному типу.
Пример:
function getFirstElement<T>(arr: T\[\]): T {
return arr\[0\];
}
const firstNumber = getFirstElement(\[1, 2, 3\]); // number
const firstString = getFirstElement(\["a", "b", "c"\]); // string
Функция getFirstElement корректно определяет возвращаемый тип в зависимости от типа элементов массива.
Применение в интерфейсах и типах
Дженерики можно использовать в интерфейсах для описания структур, которые работают с различными типами данных.
Пример:
interface Box<T> {
value: T;
}
const numberBox: Box<number> = { value: 123 };
const stringBox: Box<string> = { value: "Hello" };
Таким образом, Box становится универсальной оберткой для любого значения.
Применение в классах
Классы с дженериками позволяют создавать структуры данных или сервисы, которые остаются универсальными.
Пример:
class Repository<T> {
private items: T\[\] = \[\];
add(item: T): void {
this.items.push(item);
}
getAll(): T\[\] {
return this.items;
}
}
const numberRepo = new Repository<number>();
numberRepo.add(42);
const stringRepo = new Repository<string>();
stringRepo.add("TypeScript");
Здесь Repository может работать как с числами, так и со строками или любыми другими типами.
Ограничения дженериков (extends)
Иногда требуется, чтобы параметр типа соответствовал определенным условиям. Для этого используется оператор extends.
Пример:
interface HasId {
id: number;
}
function getId<T extends HasId>(obj: T): number {
return obj.id;
}
const user = { id: 1, name: "Alice" };
console.log(getId(user)); // 1
В данном примере функция гарантирует, что переданный объект имеет свойство id.
Польза дженериков
Использование дженериков дает несколько ключевых преимуществ:
-
Гибкость — один и тот же код можно применять к разным типам данных.
-
Строгая типизация — ошибки выявляются на этапе компиляции, так как TypeScript сохраняет информацию о типах.
-
Переиспользование — не нужно писать отдельные функции или классы для каждого типа.
-
Удобочитаемость — код становится понятнее за счет явного указания типов при использовании дженериков.