Дженерики — один из тех инструментов TypeScript, которые сначала кажутся синтаксическим украшением, а потом становятся незаменимыми. В этой статье я разложу идею по полочкам и покажу практичные примеры, как дженерики помогают писать безопасный и гибкий код.
Зачем нужны дженерики: идея и простая иллюстрация
Основная цель дженериков — сделать код повторно используемым, сохранив при этом строгую типизацию. Вместо привязки функции или типа к конкретному типу данных мы даем возможность подставлять нужный тип в момент использования.
Представьте функцию, которая возвращает элемент массива. Можно сделать ее для number, для string и т.д., а можно написать одну универсальную функцию. Это уменьшит дублирование и уберет ошибки приведения типов.
Базовый синтаксис: универсальные функции
Самый частый пример — функция identity, возвращающая переданное значение. Вот как это выглядит в TypeScript с дженериком:
function identity(value: T): T {
return value;
}
const n = identity(42); // выводится number
const s = identity("hello"); // выводится string
Здесь T — параметр типа. Когда вы вызываете функцию, компилятор либо выводит T автоматически, либо вы можете указать его явно: identity(42). Такой подход сохраняет информацию о типе и не приводит к использованию any.
Еще один полезный пример — функция, работающая с массивами:
function firstElement(arr: T[]): T | undefined {
return arr[0];
}
Тип возвращаемого значения зависит от типа элементов массива — это делает интерфейс функции выразительным и безопасным.
Дженерики в интерфейсах и типах
Дженерики полезны не только в функциях. Интерфейсы и алиасы типов тоже получают от этого большую выгоду. Например, структура «пара» может быть универсальной:
interface Pair {
first: T;
second: U;
}
const p: Pair = { first: 1, second: "one" };
В реальных проектах часто встречается шаблон репозитория или клиента API, где дженерики позволяют определить модель данных один раз и использовать ее везде:
interface Repository {
getById(id: string): Promise;
save(entity: T): Promise;
}
Такой интерфейс легко переиспользовать для разных сущностей, не теряя типовой информации о полях.
Ограничения дженериков: extends и keyof
Иногда нужно накладывать ограничения на тип-параметр, чтобы гарантировать наличие определенных свойств. Для этого служит ключевое слово extends:
function logId(obj: T) {
console.log(obj.id);
}
Здесь T должен иметь свойство id типа string. Если вызвать logId({ name: «Alice» }), компилятор выдаст ошибку. Это защищает от неверного использования функции в рантайме.
Более гибкий вариант — использовать keyof для безопасного доступа к свойствам:
function getProp(obj: T, key: K): T[K] {
return obj[key];
}
getProp гарантирует, что ключ существует в объекте, а возвращаемый тип точно соответствует типу этого свойства. Это частый паттерн, когда нужно динамически получать значения полей с сохранением типов.
Встроенные и утилитарные дженерики
TypeScript предоставляет набор стандартных утилитарных типов, основанных на дженериках. Они экономят время и снижают количество собственных шаблонов. Примеры: Partial, Readonly, Record, Pick, Omit, ReturnType.
Ниже небольшая таблица с краткими пояснениями:
| Тип | Назначение | Пример |
|---|---|---|
| Partial | Делает все свойства необязательными | Partial |
| Readonly | Делает свойства доступными только для чтения | Readonly |
| Record | Создает объект с ключами K и значениями T | Record |
Использование таких типов упрощает код и делает намерения явными. Например, Partial отлично подходит для функций обновления, где приходят лишь измененные поля.
Условные типы и infer: продвинутые техники
Условные типы позволяют выбирать тип в зависимости от другого типа. Это мощный инструмент для генерации новых типов на основе входных.
type ElementType = T extends (infer U)[] ? U : T;
type A = ElementType; // string
type B = ElementType; // number
Ключевое слово infer вводит промежуточную переменную типа, которую можно использовать внутри условного типа. С его помощью реализуют утилиты вроде ReturnType или сложные преобразования структур данных.
Эти техники встречаются в библиотеках типов и при написании обобщенных API, где нужно извлечь внутренние типы или уточнить поведение серии шаблонов.
Практические примеры из проектов
В одном из проектов мне пришлось написать обертку над fetch, возвращающую типизованный ответ. Дженерики сделали эту обертку удобной для разных эндпоинтов:
async function apiGet(url: string): Promise {
const r = await fetch(url);
return r.json() as Promise;
}
type User = { id: string; name: string };
const user = await apiGet("/api/user/1");
Такой подход избавил команду от постоянных приведения типов и упростил использование API в компонентах. Еще один пример — унифицированный обработчик форм, где типы полей формы передавались через дженерик, и это позволило избежать ошибок при чтении значений.
Советы и типичные ошибки
Ниже несколько практических замечаний, которые помогут избежать распространенных проблем при работе с дженериками.
- Не заменяйте любые типы на any. Дженерики теряют смысл, если вы сводите их к any — тогда пропадает и безопасность типов.
- Следите за выводом типов. Иногда компилятор не может вывести сложный тип, и его придется указать явно, но чаще достаточно правильно спроектировать сигнатуру функции.
- Не делайте дженерики чрезмерно сложными. Слишком запутанные ограничения усложняют поддержку кода; лучше вынести часть логики в полезные утилиты или комментарии.
- Проверяйте variance при работе с коллекциями и ковариантностью. Ошибки в предположениях о совместимости типов вызывают странные баги в крупных системах.
Если вы сомневаетесь, начните с простых и очевидных обобщений, затем постепенно переносите шаблоны в более общий вид. Часто достаточно 1-2 универсальных интерфейса, чтобы покрыть большую часть повторяющихся сценариев.
Как пробовать и где практиковать
Лучший способ понять дженерики — взять небольшой модуль и сделать его типобезопасным. Попробуйте типизировать API-клиент, инкапсулировать работу с localStorage или переработать форму. Пошаговая трансформация покажет, где шаблоны работают, а где становятся излишними.
Лично мне больше всего помогли реальные правки в кодовой базе: сначала простая обертка с дженериком, затем расширение до более сложного сценария. Через несколько итераций шаблоны стали интуитивно понятными и экономили время на отладке.
Дженерики дают возможность писать выразительный, проверяемый типами код, оставаясь гибким и масштабируемым. Попробуйте начать с простой функции и довести паттерн до полноценного интерфейса в модуле — это заметно улучшит качество вашего TypeScript-кода.

