Рано или поздно вы пишете функцию или структуру данных, которая должна работать с разными типами одинаково: обёртка ответа API, кэш, функция «вернуть первый элемент массива». Дженерики (generics) — это способ написать такой код один раз, не теряя проверку типов. Разберём, зачем они нужны и как ими пользоваться.
Зачем нужны дженерики
Представим функцию, которая возвращает первый элемент массива. Без дженериков есть два плохих варианта.
Первый — написать отдельную функцию под каждый тип. Это дублирование: firstNumber, firstString, firstUser — и так до бесконечности.
Второй — использовать any, чтобы принять что угодно:
function first(arr: any[]): any {
return arr[0];
}
const n = first([1, 2, 3]); // тип n — any, проверки потеряны
n.toUpperCase(); // компилятор молчит, а в runtime будет ошибка
Здесь мы выключили проверку типов: n имеет тип any, и TypeScript уже не подскажет, что у числа нет метода toUpperCase. Дженерики решают ровно эту проблему — позволяют связать тип результата с типом входа.
Короткая формула: дженерик — это «параметр типа», который подставляется при вызове, как обычный аргумент, только для типов.
Обобщённые функции
Перепишем first с дженериком. В угловых скобках после имени объявляем параметр типа — по соглашению его называют T (от type):
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // n: number | undefined
const s = first(["a", "b"]); // s: string | undefined
Тип T не задан заранее — он выводится из аргумента. Передали массив чисел — T стал number, и результат тоже number. Передали строки — T стал string. Один код, полная проверка типов.
Чаще всего T выводится автоматически, но при желании его можно указать явно:
const x = first<string>(["a", "b"]); // x: string | undefined
Параметров типа может быть несколько. Классический пример — функция, объединяющая два объекта:
function merge<A, B>(a: A, b: B): A & B {
return { ...a, ...b };
}
const result = merge({ name: "Ann" }, { age: 30 });
// result: { name: string } & { age: number }
result.name; // ок
result.age; // ок
Обобщённые типы и интерфейсы
Дженерики работают не только в функциях. Часто нужен обобщённый тип — структура, форма которой одинакова, а наполнение разное. Типичный случай — обёртка ответа API:
interface ApiResponse<T> {
data: T;
status: number;
error: string | null;
}
// подставляем конкретный тип при использовании
type User = { id: number; name: string };
const userResponse: ApiResponse<User> = {
data: { id: 1, name: "Ann" },
status: 200,
error: null,
};
userResponse.data.name; // тип known: string
То же самое можно записать через type:
type Box<T> = {
value: T;
};
const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: "hello" };
Параметр типа можно «прокидывать» дальше — например, обёртка для постраничного списка переиспользует ApiResponse:
type Page<T> = {
items: T[];
total: number;
};
type UsersPage = ApiResponse<Page<User>>;
Ограничения через extends
Иногда дженерик должен работать не с любым типом, а с тем, у которого есть нужное свойство. Например, функция «вернуть длину» имеет смысл только для значений, у которых есть length. Если оставить T свободным, компилятор справедливо запретит обращаться к .length — он же не знает, что оно там есть.
Решение — ограничение (constraint) через extends. Оно говорит: «T может быть любым, но обязан соответствовать вот этой форме»:
function logLength<T extends { length: number }>(value: T): T {
console.log(value.length); // теперь .length гарантированно есть
return value;
}
logLength("hello"); // ок: у строки есть length
logLength([1, 2, 3]); // ок: у массива есть length
logLength(42); // ошибка: у числа нет length
Частый приём — ограничить один параметр типа ключами другого. Оператор keyof T даёт объединение имён свойств T, и мы требуем, чтобы ключ принадлежал объекту:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Ann" };
getProperty(user, "name"); // тип результата: string
getProperty(user, "id"); // тип результата: number
getProperty(user, "email"); // ошибка: "email" нет среди ключей
T[K] здесь — это «тип свойства K у объекта T». Компилятор точно знает, что getProperty(user, "id") вернёт number, а не что попало. Это и есть переиспользуемый типобезопасный код: одна функция, корректная для любого объекта и любого его поля.
Utility types — готовые дженерики
В стандартную поставку TypeScript встроены utility types — это обобщённые типы, которые строят новый тип из существующего. Они написаны на тех же дженериках, что мы разобрали выше, и экономят много ручной работы. Несколько самых частых:
type User = {
id: number;
name: string;
email: string;
};
// Partial<T> — все поля становятся необязательными.
// Удобно для функций обновления, где меняют часть полей.
function updateUser(id: number, patch: Partial<User>) {
// patch может быть { name: "Bob" } или { email: "...", name: "..." }
}
// Pick<T, K> — выбрать только перечисленные поля.
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string }
// Record<K, V> — объект с ключами K и значениями V.
// Например, словарь "роль → список прав".
type Permissions = Record<string, string[]>;
const perms: Permissions = {
admin: ["read", "write"],
guest: ["read"],
};
Есть и другие — Omit (исключить поля), Readonly (сделать все поля доступными только для чтения), Required (сделать все обязательными). Главное — понимать, что это не магия, а обычные дженерики, и при необходимости похожие вы можете написать сами.
Коротко
- Дженерик — параметр типа, который подставляется при вызове и связывает типы входа и выхода; альтернатива дублированию кода и опасному
any. - В функциях
Tобычно выводится из аргументов автоматически, но его можно указать и явно. - Обобщёнными бывают не только функции, но и
type/interface— это форма с переменным наполнением (обёртки ответов, контейнеры, списки). - Ограничение
T extends ...сужает допустимые типы и даёт доступ к гарантированным свойствам;keyofиT[K]делают доступ к полям типобезопасным. - Utility types (
Partial,Pick,Record,Omit,Readonly) — готовые дженерики стандартной библиотеки для трансформации типов.
Что почитать дальше
- Базовые типы TypeScript — фундамент, на котором строятся дженерики.
- Сужение типов и type guards — как TypeScript уточняет тип внутри ветвлений.
- Классы и декораторы — дженерики работают и в классах, например в обобщённых репозиториях.