Generics в JavaScript: зачем нужны и как использовать для начинающих

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Что такое 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>
  );
}

Распространённые ошибки новичков

  • Избыточная типизация: не используйте дженерики, если функция работает только с одним типом
  • Игнорирование ограничений:

Блоги

Рекомендаций по книгам