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

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

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

Начать курс

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

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

Что такое Generics и зачем они нужны?

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

Основные преимущества Generics:

  • Типобезопасность — ошибки типов отлавливаются на этапе компиляции/проверки, а не в рантайме.
  • Переиспользование кода — одна функция работает с любыми типами данных.
  • Читаемость — код становится самодокументируемым.
  • Интеграция с IDE — автодополнение и подсказки типов работают корректно.

В Node.js Generics особенно полезны при работе с базами данных, API-запросами, коллекциями данных и утилитами.

Generics в TypeScript (основной способ)

TypeScript — это надстройка над JavaScript, которая добавляет статическую типизацию. Именно в TypeScript Generics реализованы наиболее полно. Рассмотрим базовый синтаксис.

Простая обобщённая функция

// Без Generics — приходится использовать any
function identity(arg: any): any {
    return arg;
}

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

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

Угловые скобки <T> — это объявление параметра типа. T — условное имя, может быть любым (часто используют T, U, V).

Generics с массивами

Один из самых частых случаев — работа с массивами:

// Функция, возвращающая первый элемент массива
function getFirst<T>(arr: T[]): T | undefined {
    return arr[0];
}

const firstNumber = getFirst([1, 2, 3]);       // number
const firstString = getFirst(["a", "b", "c"]); // string

Множественные типы

Можно использовать несколько параметров типа:

function pair<T, U>(first: T, second: U): [T, U] {
    return [first, second];
}

const result = pair(42, "ответ"); // тип [number, string]

Ограничения (Constraints) в Generics

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

// Функция, работающая только с объектами, имеющими свойство length
function getLength<T extends { length: number }>(arg: T): number {
    return arg.length;
}

getLength("hello");       // 5 — строка имеет length
getLength([1, 2, 3]);     // 3 — массив имеет length
// getLength(42);         // Ошибка! number не имеет length

Это особенно полезно в Node.js при работе с буферами, строками и массивами — всеми, у кого есть свойство length.

Generics в классах и интерфейсах

Generics отлично работают не только с функциями, но и с классами и интерфейсами.

Обобщённый класс

Пример простого стека на 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 size(): number {
        return this.items.length;
    }
}

// Использование
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 2

const stringStack = new Stack<string>();
stringStack.push("a");
stringStack.push("b");
console.log(stringStack.peek()); // "b"

Обобщённые интерфейсы

Интерфейсы с Generics помогают описывать структуры данных:

interface ApiResponse<T> {
    status: number;
    data: T;
    message?: string;
}

// Использование в Node.js (например, при работе с Express)
async function fetchUser(id: number): Promise<ApiResponse<{ name: string; email: string }>> {
    // ... запрос к базе данных
    return {
        status: 200,
        data: { name: "Иван", email: "ivan@example.com" }
    };
}

Generics в нативном JavaScript (через JSDoc)

Если вы не используете TypeScript, но хотите получить преимущества Generics в чистом JavaScript, можно использовать аннотации JSDoc. Современные IDE (VS Code, WebStorm) отлично понимают такие аннотации.

/**
 * @template T
 * @param {T[]} arr
 * @returns {T | undefined}
 */
function getLast(arr) {
    return arr[arr.length - 1];
}

// IDE будет подсказывать тип
const last = getLast([1, 2, 3]); // тип number

Этот подход особенно удобен для Node.js проектов, где TypeScript не используется, но хочется улучшить качество кода.

Практические примеры для Node.js

1. Кэширование данных

class Cache<T> {
    private store = new Map<string, T>();

    set(key: string, value: T): void {
        this.store.set(key, value);
    }

    get(key: string): T | undefined {
        return this.store.get(key);
    }

    has(key: string): boolean {
        return this.store.has(key);
    }
}

// Использование
const userCache = new Cache<{ id: number; name: string }>();
userCache.set("user1", { id: 1, name: "Анна" });

2. Обработчики событий

Блоги

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