Включите исполнение JavaScript в браузере, чтобы запустить приложение.
Разработка2 июля, 2025

Для чего использовать дженерики в TypeScript

Дженерики в TypeScript — мощный, но не самый простой инструмент. Если вы раньше использовали <T> без особого понимания, как они работают, материал поможет разобраться. Показываем, как дженерики делают код чище и надежнее.

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

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

Дженерики (или обобщения) в TypeScript — это как раз механизм для создания таких «универсальных контейнеров» в коде. Они позволяют писать компоненты: функции, классы, интерфейсы, которые могут работать с разными типами данных, но при этом сохраняют строгую типизацию и информацию об этих типах.

Вместо того, чтобы жестко указывать конкретный тип (например, string или number), мы используем параметр типа — своего рода переменную. Обычно ее обозначают одной заглавной буквой, чаще всего <T> (от Type), но можно использовать и другие (U, K, V, или даже осмысленные имена вроде TypeParameter).

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

Зачем нужны дженерики в программировании?

А зачем все усложнять? Можно же написать несколько функций для разных типов или использовать any? Давайте разберемся, какие проблемы решают дженерики:

  1. Меньше одинаковых участков кода. Предположим, вам нужна простая функция, которая принимает значение и возвращает его же (функция идентичности). Без дженериков вам пришлось бы писать отдельные функции для каждого типа:
function identityString(arg: string): string {

  return arg;

}

function identityNumber(arg: number): number {

  return arg;

}
ts

И так далее для каждого нужного типа. С дженериками вы пишете одну функцию, которая работает для всех типов.

  1. Сохранение строгой типизации и информации о типе. Можно было бы решить проблему выше с помощью any:
function identityAny(arg: any): any {

  return arg;

}

let output = identityAny("myString"); // output имеет тип any
ts

Используя any, мы теряем всю прелесть TypeScript — статическую проверку типов. Компилятор не сможет помочь нам найти ошибки, связанные с типами. Дженерики решают эту проблему:

function identity<T>(arg: T): T { // Используем дженерик T

  return arg;

}

let outputString = identity<string>("myString"); // outputString точно string

let outputNumber = identity(123); // TypeScript сам выведет тип: outputNumber - number

console.log(outputString.toUpperCase()); // TypeScript знает, что это строка

// console.log(outputNumber.toUpperCase()); // Ошибка. Компилятор сразу укажет на проблему.
ts

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

  1. Дженерики позволяют создавать абстрактные структуры данных, алгоритмы и компоненты: последние не зависят от конкретных типов данных, с которыми они будут работать. Это основа многих библиотек и фреймворков.

Синтаксис и примеры использования дженериков

Давайте подробнее рассмотрим синтаксис на примере функции идентичности:

function identity<T>(arg: T): T {

// ^        ^   ^    ^

// |        |   |    |

// |        |   |    Тип возвращаемого значения (тоже T)

// |        |   Тип аргумента (arg) - это T

// |        Параметр типа (T) - «переменная» для типа

// Имя функции

}
ts
  1. <T> после имени функции ― это объявление параметра типа. Мы говорим, что эта функция будет работать с неким типом T. T здесь — это как x в математике, просто обозначение.
  2. arg: T ― мы указываем, что ожидаем аргумент arg, и его тип будет тот самый T, который мы объявили.
  3. : T после скобок аргументов ― мы указываем, что функция вернет значение того же типа T.

Как использовать такую функцию? Есть два способа:

  • Явное указание типа. Мы прямо говорим TypeScript, какой тип будет использоваться вместо T.
let resultString = identity<string>("Hello, Generics!");

let resultNumber = identity<number>(42);
ts

Угловые скобки <string> или <number> после имени функции указывают, что в этом конкретном вызове T будет заменен на string или number.

  • В большинстве случаев TypeScript достаточно умен, чтобы самостоятельно понять, какой тип T вы подразумеваете, исходя из переданного аргумента.
let resultString = identity("Hello again!"); // TS сам поймет, что T = string

let resultNumber = identity(100);          // TS сам поймет, что T = number
ts

Это делает код короче и чище, поэтому вывод типа используется очень часто.

Функция может работать с несколькими разными типами:

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

  return [first, second];

}

let myPair = pair<string, number>("Age", 30); // Явно указали типы

console.log(myPair); // Выведет: ["Age", 30]

let anotherPair = pair(true, ["a", "b"]); // Типы выводятся: boolean и string[]

console.log(anotherPair); // Выведет: [true, ["a", "b"]]
ts

Здесь мы объявили два параметра типа <T, U> и использовали их для описания типов аргументов и возвращаемого значения (кортежа [T, U]).

Примеры создания обобщенных функций и классов

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

function getFirstElement<ElementType>(arr: ElementType[]): ElementType | undefined {

  // Используем ElementType вместо T для большей ясности

  // arr: ElementType[] - массив, состоящий из элементов типа ElementType

  // Возвращаемое значение: ElementType или undefined

  return arr.length > 0 ? arr[0] : undefined;

}

let numbers = [10, 20, 30];

let firstNum = getFirstElement(numbers); // firstNum будет number | undefined (здесь 10)

console.log(firstNum);

let strings = ["alpha", "beta", "gamma"];

let firstStr = getFirstElement(strings); // firstStr будет string | undefined (здесь "alpha")

console.log(firstStr);

let empty = [];

let firstEmpty = getFirstElement(empty); // firstEmpty будет undefined

console.log(firstEmpty);
ts

Обобщенный класс (Generic Class)

Создадим наш «универсальный контейнер», о котором говорили в начале.

class Box<ContentType> { // Объявляем параметр типа для класса

  private content: ContentType; // Свойство будет иметь тип ContentType

  constructor(initialContent: ContentType) {

    this.content = initialContent;

  }

  getContent(): ContentType {

    return this.content;

  }

}

// Создаем экземпляры Box для разных типов:

let numberBox = new Box<number>(123); // Коробка для чисел

console.log(numberBox.getContent()); // Выведет: 123

// numberBox.setContent("hello"); // Ошибка, нельзя положить строку в коробку для чисел

let stringBox = new Box("initial string"); // Тип string выводится автоматически

console.log(stringBox.getContent().toUpperCase()); // TypeScript знает, что это строка

// stringBox.setContent(true); // Ошибка, нельзя положить boolean в коробку для строк

let dateBox = new Box<Date>(new Date()); // Коробка для дат

console.log(dateBox.getContent().getFullYear()); // Oк, можем использовать методы Date
ts

Как видите, класс Box стал универсальным благодаря параметру ContentType, но при этом каждый экземпляр (numberBox, stringBox) строго типизирован.

Советы по работе с дженериками для новичков

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

  1. Не пытайтесь сразу сделать все обобщенным. Начните с ситуаций, где вы видите явное дублирование кода для разных типов, или где вам очень не хочется использовать any.
  2. Хотя T — общепринятая практика для простых случаев, для более сложных дженериков используйте имена, отражающие суть типа. Это улучшает читаемость.
  3. Не указывайте типы в угловых скобках при вызове (myFunc<string>(...)), если TypeScript может вывести их сам. Код будет чище. Делайте это явно, только если вывод не работает или для улучшения читаемости в сложных случаях.
  4. Дженерики — это стандартный инструмент в типизированных языках. Чем больше вы будете их использовать, тем понятнее они будут становиться. 
  5. Обратите внимание, как дженерики используются в популярных библиотеках. Это отличный способ увидеть их реальное применение.

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