Что такое дженерики (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 сохраняет информацию о типах.

  • Переиспользование — не нужно писать отдельные функции или классы для каждого типа.

  • Удобочитаемость — код становится понятнее за счет явного указания типов при использовании дженериков.