Бесплатный курс по TypeScript. Зарегистрируйтесь для отслеживания прогресса →

TypeScript: Дженерики (Типы)

Поговорим подробнее про Generic Types. Возьмем для примера массив. Массив — это тип-контейнер, который хранит внутри себя значения любого указанного типа. Логика работы массива не зависит от типа данных, хранящихся внутри. Такое определение автоматически говорит о том, что мы имеем дело с обобщенным типом.

Чтобы работать с таким типом, нужно конкретизировать внутренний тип в тот момент, когда мы хотим начать работу с данными этого типа:

const numbers: Array<number> = [];
numbers.push(1);

const strings: Array<string> = [];
numbers.push('hexlet');

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

Представим, что мы хотим определить свою собственную коллекцию, которая работает как массив, но с дополнительными возможностями. Такие коллекции часто делают в ORM для работы с данными, загруженными из базы. Опишем сначала конкретную версию этого типа, работающую только с числами и парой стандартных методов:

type MyColl = {
  data: Array<number>;
  forEach(callback: (value: number) => void, index: number, array: Array<number>): void;
  at(index: number): number | undefined;
}

Здесь мы видим, что данные коллекции хранятся в числовом массиве. При этом в типе определено два метода, один из которых (forEach) передает элементы коллекции в колбек, а другой (at) возвращает элементы коллекции по указанному индексу. Одна из возможных реализаций этого типа может выглядеть так:

// Типы можно не прописывать, так как они указаны в `MyColl`
const coll: MyColl = {
  data: [1, 3, 8],
  forEach(callback, index) {
    this.data.forEach(callback);
  },
  at(index) {
    return this.data.at(index);
  },
}

coll.at(-1); // 8

Теперь попробуем обобщить этот тип, то есть сделать из него дженерик. Для этого нужно сделать одну простую вещь: вместо number везде написать T (или любое другое имя, начинающееся с большой буквы) и добавить T как параметр типа к определению:

type MyColl<T> = {
  data: Array<T>;
  forEach(callback: (value: T) => void, index: T, array: Array<T>): void;
  at(index: T): T | undefined;
}

На такое определение типа можно смотреть как на своеобразное определение функции. Когда указывается конкретный тип, например так: MyColl<string>, то T в данной ситуации заменяется на string внутри определения типа. Причем если внутри типа используются другие дженерики, то они "вызывают" тип дальше. То есть все это работает как вложенные вызовы функций.

Несколько параметров

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

type Double<T, U> = {
  first: T;
  second: U;
}

const value: Double<string, number> = {
  first: 'code-basics',
  second: 1,
}

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

Задание

Реализуйте описание обощенного типа MySet, который представляет из себя аналог множества Set из JavaScript. Пример использования объекта этого типа:

const s: MySet<number> = ...
// Добавление возвращает количество элементов
s.add(1); // 1
s.add(10); // 2

s.has(1); // true
s.has(8); // false

Тип включает в себя два метода: add() и has(). Данные внутри должны храниться в свойстве items.

Упражнение не проходит проверку — что делать? 😶

Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:

  • Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет 🤨

Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.

Мой код отличается от решения учителя 🤔

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

В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.

Прочитал урок — ничего не понятно 🙄

Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.

Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.

Нашли ошибку? Есть что добавить? Пулреквесты приветствуются https://github.com/hexlet-basics
Если вы столкнулись с трудностями и не знаете, что делать, задайте вопрос в нашем большом и дружном сообществе