Дженерики - это производные типы, которые позволяют передавать типы в качестве аргументов, подобно тому, как функции принимают значения.
Они помогают создавать переиспользуемый код для типов, избавляться от дублирования и добавлять семантическую ясность. Дженерики могут применяться к типам, интерфейсам, функциям и классам. Передаются они в угловых скобках, и их количество не ограничено. Общепринято называть первый дженерик буквой T (от слова type).
Рассмотрим на примере:
function merge(obj1: object, obj2: object) {
return {
...obj1,
...obj2,
}
}
const result = merge({ a: 1, b: 2 }, { a: 2, c: 3 })
console.log(result) // {a: 2, b: 2, c: 3}Допустим, мы хотим затипизировать функцию merge, которая принимает два объекта и объединяет их ключи в результирующий объект. Если указать тип параметров как object, выходной тип тоже будет object, и TypeScript не сможет подсказать, какие ключи будут доступны после объединения.
Решение: использовать дженерики.
Теперь мы в угловые скобки функции будем принимать два дженерика T и K, куда попадут типы объектов, которые мы передаем в функцию на 13 строке (скрин 1).
Теперь TypeScript понимает, какой тип получится на выходе, и автокомплит подскажет все доступные поля.
Так как дженерики могут принимать любые типы, можно случайно передать, например, число, и это будет валидно в текущем коде. А мы бы не хотели этого допустить.
Чтобы ограничить возможные типы, используется ключевое слово extends.
Extends накладывает ограничения на передаваемые типы и позволяет писать более безопасный код. В нашем примере мы ограничили дженерики на передачу только объектов (скрин 2).
🟣Теперь давайте взглянем на другой пример.
function get(obj: object, key: string) {
return obj[key]
}
const result = get({ name: 'Andrey', age: 27 }, 'name')
console.log(result) // 'Andrey'У нас есть функция get, которая принимает объект и ключ, по которому нужно получить значение. Если типизировать её напрямую, результатом будет any.
Тут нам также помогут дженерики.
Тип объекта передаем дженериком T (скрин 3).
Ранее я уже показывал оператор keyof, который извлекает ключи объекта, с помощью него мы как раз и получаем типы ключей переданного объекта.
Но теперь мы видим, что результатом стала строка или число, хотя мы точно знаем, что если мы получаем значение по ключу name, то там должна быть строка.
В данном случае keyof извлекает все возможные значения ключей.
В нашем случае это может быть либо строка (Andrey), либо число (27).
Чтобы уточнить тип нам опять понадобится дженерик (скрин 4).
Ключ, который мы передаем вторым аргументом нужно также получать через дженерик, назовем его K. И мы обязательно должны наложить на него ограничение, что он должен быть keyof T (то есть быть одним из ключей объекта).
Теперь результирующий тип выведется во время вызова функции и правильно определится. И бонусом теперь не сможем вторым аргументом в нашу функцию передать несуществующим ключ объекта - будет ошибка.
🟣Давайте рассмотрим еще один пример, который очень часто применим во фронтенд приложениях.
Представим, что у нас есть несколько ручек бекенда, в которых структура ответа одинаковая для нескольких сущностей. Например, поля limit, pagination, size, next и есть поле items, которое является массивом этой сущности (скрин 5).
И таких сущностей и ручек у нас в проекте много, но структура всегда одна. Это создает большую проблему дублирования кода и поддержки - ведь если изменится структура пагинации, нужно будет обновлять ее во всех интерфейсах.
Написав дженерик, мы получим универсальный шаблон ответа ручек бекенда для любой сущности, нужно будет только прокинуть нужный тип (скрин 6).
Теперь у нас нет дублирование кода и легко добавлять новые сущности. Если изменится контракт бекенда - правки вносятся в одном месте, что облегчает поддержку и рефакторинг.
✈️ Telegram | 🎓 Менторство | 📹 YouTube | 👩💻 Roadmap





