Generics в JavaScript: зачем нужны и как использовать на практике

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

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

Начать курс

Введение: зачем JavaScript нужны Generics?

Разработка на JavaScript (особенно в среде Node.js) часто сталкивается с проблемой дублирования кода. Представьте, что вам нужно написать функцию, которая работает и с числами, и со строками, и с массивами. Без Generics (дженериков) вы либо пишете несколько перегрузок, либо используете any, теряя всю магию типов.

Generics — это механизм, позволяющий создавать компоненты (функции, классы, интерфейсы), которые работают с различными типами данных, сохраняя при этом строгую типизацию. В контексте JavaScript дженерики полноценно доступны через TypeScript, но понимание их принципов критично для любого современного разработчика.

В этой статье мы разберём, зачем нужны generics, как их использовать на практике в Node.js и почему это делает ваш код безопаснее и переиспользуемее.

1. Generics в TypeScript: основа типобезопасности

Хотя чистый JavaScript не имеет встроенной поддержки дженериков, TypeScript (надстройка над JS) предоставляет их в полном объёме. Именно поэтому все современные фреймворки и библиотеки для Node.js (NestJS, Express с типами) активно их используют.

Простой пример: функция identity

Классический пример — функция, которая возвращает то, что получила:

// Без generics — используем any (теряем тип)
function identity(arg: any): any {
  return arg;
}

// С generic — тип сохраняется
function identity<T>(arg: T): T {
  return arg;
}

// Использование
const num = identity<number>(42);  // Тип: number
const str = identity<string>('hello'); // Тип: string

Обратите внимание: <T> — это параметр типа. Мы говорим TypeScript: «Тип будет передан позже». Компилятор автоматически выводит тип, поэтому часто можно писать просто identity(42).

Почему это важно для Node.js?

В Node.js вы постоянно работаете с API, базами данных и внешними сервисами. Generics позволяют гарантировать, что данные приходят именно того формата, который вы ожидаете:

// Функция для работы с API
async function fetchData<T>(url: string): Promise<T> {
  const response = await fetch(url);
  return response.json() as Promise<T>;
}

// Теперь мы знаем точную структуру ответа
interface User {
  id: number;
  name: string;
}

const user = await fetchData<User>('/api/user/1');
// user.name — строка, user.id — число (TypeScript это проверяет)

2. Практические сценарии: работа с массивами и коллекциями

Generics особенно полезны при работе с массивами и коллекциями данных. Рассмотрим реальный пример из Node.js — функцию фильтрации и трансформации.

Generic-функция для маппинга

// Функция преобразует массив одного типа в массив другого типа
function mapArray<T, U>(items: T[], mapper: (item: T) => U): U[] {
  return items.map(mapper);
}

// Пример: из User получаем только имена
interface User {
  id: number;
  name: string;
  email: string;
}

const users: User[] = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' }
];

const names: string[] = mapArray(users, (user) => user.name);
// names: ['Alice', 'Bob'] — тип string[] гарантирован

Здесь <T, U> — два параметра типа. T — тип входного массива, U — тип результата. Это даёт максимальную гибкость без потери типов.

Generic-класс для стека

В Node.js часто нужны структуры данных. Создадим типобезопасный стек:

class Stack<T> {
  private items: T[] = [];

  push(item: T): void {
    this.items.push(item);
  }

  pop(): T | undefined {
    return this.items.pop();
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }

  get length(): number {
    return this.items.length;
  }
}

// Использование
const numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
const top = numberStack.pop(); // top: number | undefined

const stringStack = new Stack<string>();
// stringStack.push(42); // Ошибка! Только строки

Этот класс можно использовать для любых типов данных, и TypeScript будет проверять корректность на этапе компиляции.

3. Ограничения дженериков (Constraints)

Иногда нужно ограничить, какие типы могут быть использованы в generic. Для этого используются constraints (ограничения).

Пример: работа с объектами, имеющими id

// Ограничиваем T — он должен иметь поле id
interface HasId {
  id: number;
}

function findById<T extends HasId>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id);
}

// Работает
interface Product extends HasId {
  name: string;
  price: number;
}

const products: Product[] = [
  { id: 1, name: 'Laptop', price: 1000 },
  { id: 2, name: 'Mouse', price: 50 }
];

const laptop = findById(products, 1); // Тип: Product | undefined

// Не сработает с типом без id
// findById([1, 2, 3], 1); // Ошибка! number не имеет поля id

Это особенно полезно при работе с базами данных в Node.js. Вы можете создать общий репозиторий, который работает с любой сущностью, имеющей id:

class BaseRepository<T extends HasId> {
  constructor(private items: T[]) {}

  getById(id: number): T | undefined {
    return this.items.find(item => item.id === id);
  }

  getAll(): T[] {
    return [...this.items];
  }

  add(item: T): void {
    this.items.push(item);
  }
}

// Теперь для User и Product — один класс
const userRepo = new BaseRepository<User>(users);
const productRepo = new BaseRepository<Product>(products);

4. Generics и утилитарные типы в Node.js

В TypeScript

Блоги

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