Дженерики (generic) помогают писать универсальный, переиспользуемый код, а также в некоторых случаях позволяют отказаться от any. Главная задача дженериков — помочь разработчику писать код, который одинаково будет работать со значениями разных типов.

Посмотрим на примере из реального мира.

Пример из реального мира

Представьте завод по изготовлению автомобилей. Старый завод, который проектировался для сборки автомобиля определённой модели. На нём могут собирать только такую модель автомобиля, а если потребуется выпустить машину с немного другим кузовом, то придётся строить новый завод. Это неоптимальное решение. Если разные машины собираются одинаково, то лучше научиться собирать разные машины на одном заводе.

Кажется очевидным, но реальность такова:

❌ Строить новый завод для каждой модели машины

✅ Собирать разные машины на одном заводе

Суть дженериков

С дженериками тоже примерно так. Если мы напишем функцию и жёстко зададим тип, то она сможет работать только со значениями этого типа. Значения других типов передать не получится. Есть два способа это поправить.

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

function includeStr(array: string[], query:string): boolean {
    // на входе массив и строка для поиска
    for (const value of array) {
    // перебираем массив
        if (value === query) {
            // если в массиве есть элемент — возвращаем true
            return true;
        }
    }
  // если ничего не нашлось, возвращаем false
  return false;
}

Функция будет отлично работать на массивах из строк. Но для поиска в массиве из чисел придётся дублировать функцию, менять типы, но сам код функции останется неизменным. Например:

function includeNumber(array: number[], query:number): boolean {
    // всё то же самое, только на входе числа
    for (const value of array) {
        if (value === query) {
            return true;
        }
    }
    return false;
}

Вот тут на помощь и приходят дженерики. Они помогают написать код, который одинаково работает с данными разных типов.

❌ Пишем много функций для разных типов

✅ Объявляем в функции параметр типа, а потом передаём через него нужный тип

Вместо конкретного типа мы как будто объявляем «переменную», а затем передаём в неё нужный тип. Таким образом, получается код, который может работать с разными типами:

function include<T>(array: T[], query:T): boolean {
    for (const value of array) {
        if (value === query) {
            return true;
        }
    }
    return false;
}

Код функции не поменялся, но теперь мы не указываем конкретный тип. Мы заводим переменную T и говорим, что тип параметра array — это тип, который будет передан в переменную T. А тип параметра query — это тип, который будет передан через переменную T.

Когда мы захотим воспользоваться этой функцией, то помимо данных для параметров array и query мы ещё должны передать информацию о типах (для переменной T). В первом примере мы передаём тип string, а во втором — number.

// передаём string в качестве типа
include<string>(['igor', 'sasha', 'ira'], 'ira'); // true

// передаём number в качестве типа
include<number>([1, 3, 5], 7); // false

Получается, что с помощью дженериков мы смогли написать код, который работает с разными типами значений. То есть, если коротко.

👉 Дженерики — переменные, через которые мы передаём тип.

Ещё о JavaScript