Что такое Generics в JavaScript и зачем они нужны?
Представьте, что вы пишете функцию, которая должна работать с разными типами данных — числами, строками, объектами. Без Generics (дженериков) вам пришлось бы либо дублировать код, либо использовать any, теряя контроль над типами. Generics — это механизм, который позволяет создавать компоненты (функции, классы, интерфейсы), работающие с любым типом данных, но при этом сохраняющие информацию о типе.
Хотя JavaScript — язык с динамической типизацией, в экосистеме Node.js активно используется TypeScript, который добавляет статическую типизацию. Именно в TypeScript дженерики раскрываются в полную силу. Однако понимать их принципы полезно и для чистого JavaScript — особенно если вы пишете библиотеки или работаете с современными фреймворками.
В этой статье мы разберём:
- Зачем нужны Generics
- Как объявлять и использовать дженерики в TypeScript
- Примеры для Node.js (API, обработка данных)
- Ограничения и лучшие практики
Основы Generics: синтаксис и базовые примеры
Первый дженерик: функция identity
Классический пример — функция, которая возвращает то, что получила на вход:
// Без Generics
function identity(value: any): any {
return value;
}
// С Generics
function identity<T>(value: T): T {
return value;
}
// Использование
const num = identity<number>(42); // num — number
const str = identity<string>('hello'); // str — string
Символ <T> — это параметр типа. Вы можете называть его как угодно (T, U, Item), но традиция требует использовать T (от Type).
Дженерики с несколькими параметрами
Иногда нужно работать с несколькими типами одновременно:
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const result = pair<number, string>(1, 'один');
// result: [number, string]
Дженерики в стрелочных функциях (Node.js)
В React или утилитах часто используют стрелочные функции с дженериками:
const wrapInArray = <T>(item: T): T[] => {
return [item];
};
const arr = wrapInArray('test'); // arr: string[]
Применение Generics в реальных проектах Node.js
API-обработчики с типизированными ответами
Представьте, что вы пишете универсальную функцию для запросов к API. Без дженериков вы бы потеряли типы данных:
// Плохо: any
async function fetchData(url: string): Promise<any> {
const response = await fetch(url);
return response.json();
}
// Хорошо: Generics
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json();
}
// Использование
interface User {
id: number;
name: string;
}
const user = await fetchData<User>('/api/user/1');
console.log(user.name); // TypeScript знает, что name — string
Типизированные коллекции данных
При работе с базами данных (например, через Prisma или Mongoose) дженерики помогают создавать универсальные репозитории:
class Repository<T extends { id: number }> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
getById(id: number): T | undefined {
return this.items.find(item => item.id === id);
}
}
// Использование
interface Product {
id: number;
title: string;
price: number;
}
const productRepo = new Repository<Product>();
productRepo.add({ id: 1, title: 'Laptop', price: 1000 });
const laptop = productRepo.getById(1);
Обёртки для обработки ошибок
Универсальная функция для безопасного выполнения операций:
function safeExecute<T>(fn: () => T): { data: T | null; error: Error | null } {
try {
const data = fn();
return { data, error: null };
} catch (error) {
return { data: null, error: error as Error };
}
}
// Использование
const result = safeExecute(() => JSON.parse('{"valid": true}'));
if (result.error) {
console.error('Ошибка парсинга');
} else {
console.log(result.data?.valid); // true
}
Ограничения Generics и продвинутые техники
Ограничения с помощью extends
Иногда нужно, чтобы дженерик работал только с определёнными типами. Для этого используется extends:
// Только типы, у которых есть свойство length
function logLength<T extends { length: number }>(item: T): void {
console.log(item.length);
}
logLength('hello'); // 5
logLength([1, 2, 3]); // 3
// logLength(123); // Ошибка: у number нет length
Условные типы (Conditional Types)
Позволяют менять тип на основе условия:
type IsString<T> = T extends string ? 'yes' : 'no';
type Test1 = IsString<string>; // 'yes'
type Test2 = IsString<number>; // 'no'
Дженерики в интерфейсах React-компонентов
Хотя React — это не Node.js, в современных fullstack-приложениях (Next.js) это актуально:
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>(props: ListProps<T>) {
return (
<ul>
{props.items.map((item, index) => (
<li key={index}>{props.renderItem(item)}</li>
))}
</ul>
);
}
Распространённые ошибки новичков
- Избыточная типизация: не используйте дженерики, если функция работает только с одним типом
- Игнорирование ограничений: