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

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

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

Начать курс

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

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

В контексте JavaScript дженерики особенно актуальны при работе с TypeScript или при использовании современных паттернов в Node.js. Даже если вы пишете на чистом JavaScript, понимание концепции Generics поможет вам писать более гибкий и безопасный код.

В этой статье мы разберём:

  • Что такое Generics и зачем они нужны
  • Как использовать дженерики в функциях и классах
  • Практические примеры для Node.js
  • Распространённые ошибки новичков

Что такое Generics (дженерики) в JavaScript?

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

Представьте, что вы создаёте коробку. Обычная коробка может хранить только один тип предметов. Дженерики — это "умная" коробка, которая подстраивается под то, что вы в неё кладёте, но при этом помнит, что именно там лежит.

В TypeScript (надстройке над JavaScript) дженерики обозначаются угловыми скобками <> и буквой T (от Type) или другими идентификаторами:

// Простейший пример дженерика в TypeScript
function identity<T>(arg: T): T {
    return arg;
}

// Теперь функция работает с любым типом
const num = identity<number>(42);    // тип number
const str = identity<string>("Hello"); // тип string

В чистом JavaScript такой синтаксис недоступен, но концепция остаётся полезной для понимания архитектуры приложений.

Зачем нужны Generics в JavaScript и Node.js?

Рассмотрим три ключевые причины, по которым стоит разобраться с дженериками.

1. Безопасность типов без потери гибкости

Без дженериков вы часто используете any, что отключает проверку типов:

// Плохо: теряем информацию о типе
function getFirstElement(arr: any[]): any {
    return arr[0];
}

const val = getFirstElement([1, 2, 3]);
// val имеет тип any, мы не знаем, число это или строка

С дженериками тип сохраняется:

// Хорошо: тип сохраняется
function getFirstElement<T>(arr: T[]): T {
    return arr[0];
}

const val = getFirstElement([1, 2, 3]); // val: number
const str = getFirstElement(["a", "b"]); // str: string

2. Уменьшение дублирования кода

Без дженериков вам пришлось бы писать отдельные функции для каждого типа:

// Без дженериков — дублирование
function processNumber(num: number): number { /* ... */ }
function processString(str: string): string { /* ... */ }
function processObject(obj: object): object { /* ... */ }

// С дженериками — одна функция
function process<T>(data: T): T { /* ... */ }

3. Создание переиспользуемых компонентов

В Node.js дженерики часто используются для создания универсальных утилит, обработчиков данных и API-клиентов:

// Универсальный HTTP-клиент с дженериками
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 автоматически имеет тип User

Как использовать Generics: практические примеры для Node.js

Давайте рассмотрим несколько реальных сценариев использования дженериков в проектах на Node.js.

Пример 1: Универсальная функция кэширования

Создадим функцию, которая кэширует результаты любых асинхронных операций:

// cache.ts
function createCache<T>() {
    const cache = new Map<string, T>();
    
    return {
        get(key: string): T | undefined {
            return cache.get(key);
        },
        set(key: string, value: T): void {
            cache.set(key, value);
        },
        clear(): void {
            cache.clear();
        }
    };
}

// Использование
const userCache = createCache<User>();
userCache.set('user_1', { id: 1, name: 'Alice' });
const cachedUser = userCache.get('user_1'); // тип User | undefined

Пример 2: Обработчик API-ответов

Часто API возвращает обёрнутые ответы. Дженерики помогают сохранить типы:

// api-response.ts
interface ApiResponse<T> {
    success: boolean;
    data: T;
    error?: string;
    timestamp: number;
}

async function handleApiResponse<T>(response: Response): Promise<ApiResponse<T>> {
    const json = await response.json();
    
    if (!response.ok) {
        return {
            success: false,
            data: null as unknown as T,
            error: json.message || 'Unknown error',
            timestamp: Date.now()
        };
    }
    
    return {
        success: true,
        data: json as T,
        timestamp: Date.now()
    };
}

// Использование
const result = await handleApiResponse<User[]>(response);
if (result.success) {
    console.log(result.data.length); // TypeScript знает, что это массив User
}

Пример 3: Универсальный репозиторий для базы данных

В Node.js приложениях часто используют паттерн Repository. Дженерики делают его универсальным:

// repository.ts
interface HasId {
    id: number;
}

class BaseRepository<T extends HasId> {
    private items: T[] = [];
    
    findById(id: number): T | undefined {
        return this.items.find(item => item.id === id);
    }
    
    save(item: T): void {
        const index = this.items.findIndex(i => i.id === item.id);
        if (index !== -1) {
            this.items[index] = item;
        } else {
            this.items.push(item);                

Блоги

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