TypeScript дженерики в повседневном коде: практическое руководство

Frontend2026-09-21TryQuickToolBox

Вы пишете функцию, которая работает с любым типом, но не хотите терять типобезопасность. Можно было бы использовать any, но это противоречит смыслу TypeScript. Дженерики позволяют писать код, работающий с несколькими типами, сохраняя информацию о типах. Это руководство покажет, как использовать дженерики в повседневном коде, не усложняя его.

Что такое дженерики?

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

Думайте о дженериках как о параметрах типа. Так же как функция принимает параметры-значения, generic-функция принимает параметры-типы. Например, функция идентичности:

function identity<T>(arg: T): T {
  return arg;
}

Здесь T — это параметр типа. Когда вы вызываете identity<string>("hello"), TypeScript выводит, что T — это string, и тип возвращаемого значения — string. Если передать число, вернётся число. Это точнее, чем использование any.

Почему бы просто не использовать any?

Использование any отключает проверку типов. Вы теряете автодополнение, поддержку рефакторинга, и компилятор не может поймать ошибки. Дженерики сохраняют информацию о типах на протяжении всего кода, так что вы получаете и гибкость, и безопасность.

Generic-функции на практике

Рассмотрим распространённый сценарий: функция, возвращающая первый элемент массива.

function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

Без дженериков вы могли бы написать function firstElement(arr: any[]): any, что теряет тип. С дженериками, если передать string[], тип возвращаемого значения — string | undefined.

Можно также использовать несколько параметров типа. Например, функция, которая преобразует массив в новый массив с помощью колбэка:

function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
  return arr.map(fn);
}

Именно так Array.prototype.map типизирован в стандартной библиотеке TypeScript.

Generic-интерфейсы и псевдонимы типов

Дженерики не ограничиваются функциями. Вы можете создавать generic-интерфейсы и псевдонимы типов для определения переиспользуемых структур данных.

interface Box<T> {
  value: T;
}

const stringBox: Box<string> = { value: "hello" };
const numberBox: Box<number> = { value: 42 };

Это полезно для ответов API, управления состоянием или любого типа-контейнера.

Можно также задавать типы по умолчанию:

interface ApiResponse<T = unknown> {
  data: T;
  status: number;
}

Теперь ApiResponse без аргумента типа по умолчанию использует unknown.

Ограничения дженериков

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

function getLength<T extends { length: number }>(arg: T): number {
  return arg.length;
}

Теперь T должен иметь свойство length. Можно вызвать getLength("hello") или getLength([1,2,3]), но не getLength(42).

Ограничения мощны при работе с объектами, имеющими общую форму.

Использование дженериков с классами

Классы тоже могут быть generic. Распространённый пример — generic-коллекция:

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

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

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

const stack = new Stack<number>();
stack.push(1);
stack.push(2);
const popped = stack.pop(); // number | undefined

Это гарантирует, что в числовой стек можно добавлять только числа.

Реальный пример: типобезопасный эмиттер событий

Давайте создадим простой эмиттер событий, использующий дженерики для сопоставления имён событий с типами полезной нагрузки.

type EventMap = Record<string, any>;

class EventEmitter<T extends EventMap> {
  private listeners: { [K in keyof T]?: Array<(payload: T[K]) => void> } = {};

  on<K extends keyof T>(event: K, listener: (payload: T[K]) => void): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(listener);
  }

  emit<K extends keyof T>(event: K, payload: T[K]): void {
    this.listeners[event]?.forEach(listener => listener(payload));
  }
}

// Использование
interface MyEvents {
  login: { userId: string };
  logout: { userId: string };
}

const emitter = new EventEmitter<MyEvents>();
emitter.on('login', (payload) => {
  console.log(payload.userId); // string
});
emitter.emit('login', { userId: '123' }); // OK
// emitter.emit('login', { userId: 123 }); // Ошибка

Этот шаблон даёт полную типобезопасность для имён событий и полезной нагрузки, снижая количество ошибок во время выполнения.

Типичные ошибки и лучшие практики

Сравнение: дженерики vs. any vs. union-типы

Подход Типобезопасность Гибкость Случай использования
Дженерики Высокая Высокая Переиспользуемые компоненты с сохранением типов
any Отсутствует Высокая Быстрое прототипирование, миграция с JS
Union-типы Средняя Средняя Ограниченный набор известных типов

Дженерики находят баланс между гибкостью и безопасностью, что делает их идеальными для библиотек и общих утилит.

FAQ

Когда использовать дженерики вместо union-типов?

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

Можно ли использовать дженерики со стрелочными функциями?

Да, но синтаксис может быть сложным в .tsx файлах. Можно написать const identity = <T,>(arg: T): T => arg; с завершающей запятой, чтобы избежать неоднозначности с JSX.

Как ограничить дженерик только объектами?

Используйте T extends object или T extends Record<string, unknown>, чтобы гарантировать, что T — объектный тип.

Дженерики — краеугольный камень системы типов TypeScript. Используя их в повседневном коде, вы можете писать функции, интерфейсы и классы, которые одновременно гибкие и безопасные. Начните с простых generic-функций, затем постепенно добавляйте ограничения и более сложные шаблоны по мере необходимости. Больше инструментов для разработчиков — в нашем JSON Formatter для быстрой проверки и форматирования JSON-данных.