TypeScript Generics для повседневного кода

Frontend2026-10-05TryQuickToolBox

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

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

Что такое Generics?

Generics — это способ параметризации типов. Точно так же, как функции принимают параметры, generics принимают параметры типов. Они позволяют создавать переиспользуемые компоненты, которые работают с множеством типов, а не с одним.

Рассмотрим эту функцию идентичности:

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

Здесь T — это переменная типа. Когда вы вызываете identity(42), TypeScript выводит T как number. Когда вы вызываете identity("hello"), T становится string. Тип возвращаемого значения всегда совпадает с типом входного.

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

Давайте рассмотрим распространённый сценарий: функция, которая оборачивает значение в объект.

function wrapInObject<T>(value: T): { value: T } {
  return { value };
}

Без generics вам пришлось бы использовать any или unknown, и вызывающий код потерял бы информацию о типе. С generics тип возвращаемого значения точно равен { value: T }.

Другой повседневный пример — функция, возвращающая последний элемент массива:

function last<T>(arr: T[]): T | undefined {
  return arr[arr.length - 1];
}

TypeScript выводит T из аргумента-массива, поэтому last([1, 2, 3]) возвращает number | undefined.

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

Иногда вы хотите ограничить типы, которые можно использовать с вашим generic. Например, вам может понадобиться доступ к свойству, которое существует только у определённых типов. Вы можете использовать ключевое слово extends, чтобы добавить ограничение.

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

Теперь T должен иметь свойство length типа number. Это работает для строк, массивов и любого объекта со свойством length.

Вы также можете использовать ограничения с интерфейсами:

interface HasId {
  id: string;
}

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

Это гарантирует, что элементы, передаваемые в findById, имеют свойство id.

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

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

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

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  const response = await fetch(url);
  const data = await response.json();
  return { data, status: response.status };
}

Теперь вы можете точно типизировать ответы API:

interface User {
  id: number;
  name: string;
}

const userResponse = await fetchData<User>('/api/user');
// userResponse.data is User

Generic-классы

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

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];
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
const num = numberStack.pop(); // number | undefined

Generics в React-компонентах

Если вы создаёте React-компоненты, generics могут сделать их более переиспользуемыми. Например, компонент списка, который рендерит элементы любого типа:

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}

function List<T>({ items, renderItem }: ListProps<T>) {
  return <ul>{items.map((item, index) => <li key={index}>{renderItem(item)}</li>)}</ul>;
}

Теперь вы можете использовать List с любым типом и получить полную проверку типов для колбэка renderItem.

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

Сравнение: Generics vs. any vs. Union-типы

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

FAQ

Когда следует использовать generics вместо any?

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

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

Да. Вы можете определять функции с несколькими параметрами типов, например function pair<K, V>(key: K, value: V): [K, V]. Используйте описательные имена, когда их больше одного.

Как generics работают со стрелочными функциями в файлах .tsx?

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

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