Антон Ларичев

Введение
TypeScript дженерики (generics) — один из самых мощных инструментов системы типов. Они позволяют писать переиспользуемый код, который работает с разными типами данных, сохраняя при этом строгую типизацию. Если функции и классы без дженериков жёстко привязаны к конкретным типам, то обобщённые конструкции принимают тип как параметр и возвращают результат, точно соответствующий входным данным.
В этом гайде разберём синтаксис, ограничения (constraints), условные типы, утилитные дженерики и практические примеры, которые встречаются в реальной работе с React, Node.js и библиотеками вроде Axios или Prisma.
Базовый синтаксис дженериков
Простейший пример — функция, которая возвращает то, что в неё передали. Без дженериков пришлось бы либо использовать any и потерять типизацию, либо писать перегрузки для каждого типа.
// Функция принимает значение типа T и возвращает то же T
function identity<T>(value: T): T {
return value;
}
// TypeScript сам выведет тип: number
const num = identity(42);
// Тип можно указать явно
const str = identity<string>("hello");
Дженерики можно использовать в интерфейсах, типах и классах:
// Обобщённый интерфейс ответа API
interface ApiResponse<T> {
data: T;
status: number;
error: string | null;
}
// Конкретизация под пользователя
type UserResponse = ApiResponse<{ id: number; name: string }>;
Ограничения через extends
Иногда нужно гарантировать, что тип-параметр обладает определённой структурой. Для этого применяется ключевое слово extends.
// T должен иметь поле length
function logLength<T extends { length: number }>(value: T): T {
console.log(value.length);
return value;
}
logLength("строка"); // ок, у строки есть length
logLength([1, 2, 3]); // ок, у массива есть length
// logLength(42); // ошибка: у number нет length
Ещё полезный паттерн — получение типа ключа объекта через keyof:
// K должен быть одним из ключей объекта T
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Anna", role: "admin" };
const name = getProperty(user, "name"); // тип string
// getProperty(user, "email"); // ошибка: нет такого ключа
Значения по умолчанию
Для параметров типа можно задать значение по умолчанию — как для обычных аргументов функций. Это упрощает работу с обобщёнными компонентами.
// По умолчанию состояние — пустой объект
interface State<T = Record<string, unknown>> {
loading: boolean;
data: T;
}
// Без явного указания типа
const initial: State = { loading: false, data: {} };
Условные типы и infer
Условные типы выглядят как тернарный оператор, но работают на уровне типов. Они особенно полезны в сочетании с infer — для извлечения вложенных типов.
// Извлекаем тип элемента массива
type ElementOf<T> = T extends Array<infer U> ? U : never;
type Numbers = ElementOf<number[]>; // number
type Mixed = ElementOf<(string | boolean)[]>; // string | boolean
Типичный сценарий — получение типа возвращаемого значения функции (это встроенный ReturnType):
// Реализация ReturnType вручную
type MyReturnType<T extends (...args: any[]) => any> =
T extends (...args: any[]) => infer R ? R : never;
function fetchUser() {
return { id: 1, name: "Bob" };
}
type User = MyReturnType<typeof fetchUser>; // { id: number; name: string }
Утилитные дженерики
TypeScript предоставляет набор встроенных обобщённых типов, которые покрывают большинство задач трансформации.
interface Article {
id: number;
title: string;
body: string;
publishedAt: Date;
}
// Все поля становятся необязательными
type ArticleDraft = Partial<Article>;
// Только нужные поля
type ArticlePreview = Pick<Article, "id" | "title">;
// Все поля кроме указанных
type ArticleWithoutDate = Omit<Article, "publishedAt">;
// Делает все поля обязательными
type StrictArticle = Required<Article>;
Практический пример: типобезопасный fetch
Соберём небольшую обёртку над fetch, которая возвращает данные нужного типа без приведения через as.
// Обобщённая функция запроса
async function apiGet<T>(url: string): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json() as Promise<T>;
}
// Использование с указанием формы ответа
interface Post {
id: number;
title: string;
}
const posts = await apiGet<Post[]>("/api/posts");
posts[0].title; // автодополнение и проверка типов
Дженерики в React-компонентах
Обобщённые компоненты помогают строить переиспользуемые списки, таблицы и селекты без потери типизации элементов.
// Список с произвольным типом элементов
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map((item, i) => <li key={i}>{renderItem(item)}</li>)}</ul>;
}
// item автоматически имеет тип { id: number; name: string }
<List
items={[{ id: 1, name: "Anna" }]}
renderItem={(item) => item.name}
/>;
Частые ошибки
Чрезмерное обобщение. Не каждой функции нужны дженерики. Если параметр типа используется только один раз и не влияет на возврат — это просто шум. Заменяйте таким случаем обычным типом.
Использование any вместо ограничений. Конструкция <T extends any> или T = any обнуляет пользу от обобщения. Применяйте unknown, если действительно не знаете тип, или задавайте осмысленные ограничения через extends.
Игнорирование вывода типов. TypeScript умеет выводить параметры из аргументов. Явное указание identity<string>("x") избыточно, если компилятор сам справляется. Это засоряет код.
Путаница с keyof и значением ключа. Тип T[K] — это тип значения, а не строки ключа. Помните, что ключ остаётся литералом, а через индексный доступ вы получаете именно поле.
Дженерик вместо union. Если функция принимает строго два варианта типа, перегрузки или union читаются понятнее, чем обобщение.
Заключение
Дженерики превращают TypeScript из строгого, но негибкого языка в выразительный инструмент моделирования данных. Они позволяют описать связь между входом и выходом функции, между состоянием и его трансформациями, между ключом объекта и типом значения. Начните с простых случаев — обёрток над API, утилит для коллекций, компонентов-списков — и постепенно подключайте условные типы и infer, когда библиотечный код требует точного описания форм данных. Главное правило: дженерик должен делать код безопаснее и читаемее, а не наоборот.






Комментарии
0