Дженерики: функции и классы
Код, который работает с любым типом, но не теряет проверку типов — в этом сила дженериков.
Дженерик — параметр типа, который подставляется при вызове, позволяя одной функции или классу работать с разными типами безопасно.
Проблема, которую решают дженерики
Допустим, нужна функция, возвращающая первый элемент массива. Если массив чисел — хотим число, если строк — строку. С any можно, но это убивает типы:
function firstAny(arr: any[]): any {
return arr[0];
}
const n = firstAny([1, 2, 3]); // тип n — any, проверка потеряна
n.toUpperCase(); // компилятор молчит, а в рантайме упадёт
С any функция универсальна, но небезопасна. Хочется и универсальность, и точный тип результата. Это и дают дженерики.
Дженерик-функция
Параметр типа объявляется в угловых скобках — по соглашению его называют T (от Type). Он связывает тип аргумента с типом результата:
function first<T>(arr: T[]): T {
return arr[0];
}
const num = first([1, 2, 3]); // T выведен как number → num: number
const str = first(["a", "b", "c"]); // T выведен как string → str: string
str.toUpperCase(); // ок — TypeScript знает, что это строка
num.toUpperCase(); // Ошибка: Property 'toUpperCase' does not exist on type 'number'.
Тип T подставляется автоматически из аргумента. Одна функция, любой тип, полная типобезопасность — никакого any.
Ограничения дженериков
Иногда нужно, чтобы тип обладал определёнными свойствами. Ключевое слово extends ограничивает T:
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("кот", "пёсик"); // ок: у строк есть length
longest([1, 2], [1, 2, 3]); // ок: у массивов есть length
longest(10, 20); // Ошибка: у number нет свойства length
Теперь T — «любой тип, у которого есть length», и внутри функции можно безопасно к нему обращаться.
Дженерик-класс
Классы тоже бывают дженериками. Классический пример — типобезопасный контейнер:
class Box<T> {
private content: T;
constructor(value: T) {
this.content = value;
}
get(): T {
return this.content;
}
}
const numberBox = new Box<number>(42);
const stringBox = new Box<string>("привет");
const x: number = numberBox.get(); // ок
const y: number = stringBox.get(); // Ошибка: тип string не присваивается number
Тип задаётся при создании (Box<number>), и весь класс настраивается под него. Так пишут переиспользуемые структуры: коллекции, кэши, очереди — один раз и для любого типа.
Зачем это на практике
Дженерики — основа типизированных библиотек. Массивы (Array<T>), промисы (Promise<T>), коллекции — всё это дженерики. Они дают переиспользование без копипасты и без жертвы типобезопасностью.
Итог
- Дженерик
<T>— параметр типа: одна функция/класс работает с любым типом, сохраняя проверку. - Тип
Tобычно выводится из аргументов автоматически; ограничениеT extends ...требует нужных свойств. - Дженерики заменяют небезопасный
anyтам, где код должен быть универсальным.