Дженерики: функции и классы

Код, который работает с любым типом, но не теряет проверку типов — в этом сила дженериков.

Дженерик — параметр типа, который подставляется при вызове, позволяя одной функции или классу работать с разными типами безопасно.

Проблема, которую решают дженерики

Допустим, нужна функция, возвращающая первый элемент массива. Если массив чисел — хотим число, если строк — строку. С any можно, но это убивает типы:

function firstAny(arr: any[]): any {
  return arr[0];
}

const n = firstAny([1, 2, 3]); // тип n — any, проверка потеряна
n.toUpperCase();               // компилятор молчит, а в рантайме упадёт

С any функция универсальна, но небезопасна. Хочется и универсальность, и точный тип результата. Это и дают дженерики.

Дженерик-функция

Параметр типа объявляется в угловых скобках — по соглашению его называют T (от Type). Он связывает тип аргумента с типом результата:

function first<T>(arr: T[]): T {
  return arr[0];
}

const num = first([1, 2, 3]);        // T выведен как number → num: number
const str = first(["a", "b", "c"]);  // T выведен как string → str: string

str.toUpperCase(); // ок — TypeScript знает, что это строка
num.toUpperCase(); // Ошибка: Property 'toUpperCase' does not exist on type 'number'.

Тип T подставляется автоматически из аргумента. Одна функция, любой тип, полная типобезопасность — никакого any.

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

Иногда нужно, чтобы тип обладал определёнными свойствами. Ключевое слово extends ограничивает T:

function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

longest("кот", "пёсик");      // ок: у строк есть length
longest([1, 2], [1, 2, 3]);   // ок: у массивов есть length
longest(10, 20);              // Ошибка: у number нет свойства length

Теперь T — «любой тип, у которого есть length», и внутри функции можно безопасно к нему обращаться.

Дженерик-класс

Классы тоже бывают дженериками. Классический пример — типобезопасный контейнер:

class Box<T> {
  private content: T;

  constructor(value: T) {
    this.content = value;
  }

  get(): T {
    return this.content;
  }
}

const numberBox = new Box<number>(42);
const stringBox = new Box<string>("привет");

const x: number = numberBox.get(); // ок
const y: number = stringBox.get(); // Ошибка: тип string не присваивается number

Тип задаётся при создании (Box<number>), и весь класс настраивается под него. Так пишут переиспользуемые структуры: коллекции, кэши, очереди — один раз и для любого типа.

Зачем это на практике

Дженерики — основа типизированных библиотек. Массивы (Array<T>), промисы (Promise<T>), коллекции — всё это дженерики. Они дают переиспользование без копипасты и без жертвы типобезопасностью.

Итог

  • Дженерик <T> — параметр типа: одна функция/класс работает с любым типом, сохраняя проверку.
  • Тип T обычно выводится из аргументов автоматически; ограничение T extends ... требует нужных свойств.
  • Дженерики заменяют небезопасный any там, где код должен быть универсальным.
Проверьте себя
1. Чем дженерик лучше any для универсальной функции?
AДженерик работает быстрее
BДженерик сохраняет связь типов: тип результата выводится из аргумента, проверка не теряется
Cany нельзя использовать в функциях
DМежду ними нет разницы
2. Что означает ограничение <T extends { length: number }>?
AT должен быть массивом
BT — любой тип, у которого есть свойство length
CT наследуется от класса length
DT обязан быть числом
3. Откуда берётся конкретный тип T при вызове first([1, 2, 3])?
AЕго всегда нужно указывать вручную
BTypeScript выводит его автоматически из аргумента (здесь number)
CT всегда равен any
DT определяется типом возвращаемого значения переменной
Поддержать проект