В этом уроке поговорим подробнее про Generic Types. Возьмем для примера массив.
Массив — это тип-контейнер, который хранит внутри себя значения любого указанного типа. Логика работы массива не зависит от типа данных, хранящихся внутри. Такое определение автоматически говорит о том, что мы имеем дело с обобщенным типом.
Чтобы работать с таким типом, нужно конкретизировать внутренний тип в тот момент, когда мы хотим начать работу с данными этого типа:
const numbers: Array<number> = [];
numbers.push(1);
const strings: Array<string> = [];
strings.push('hexlet');
Тип, который указывается внутри угловых скобок, называется параметром типа. Такое название выбрано неслучайно — указание параметра выглядит как вызов функции. Ниже мы увидим, что такой взгляд на дженерики помогает лучше понять их принцип работы.
Представим, что мы хотим определить свою коллекцию, которая работает как массив, но с дополнительными возможностями. Такие коллекции часто делают в ORM для работы с данными, загруженными из базы. Опишем сначала конкретную версию этого типа, работающую только с числами и парой стандартных методов:
type MyColl = {
data: Array<number>;
forEach(callback: (value: number, index: number, array: Array<number>) => void): void;
at(index: number): number | undefined;
}
Здесь мы видим, что данные коллекции хранятся в числовом массиве. При этом в типе определено два метода:
- Метод
forEach
передает элементы коллекции в колбек - Метод
at
возвращает элементы коллекции по указанному индексу
Одна из возможных реализаций этого типа может выглядеть так:
// Типы можно не прописывать, потому что они указаны в MyColl
const coll: MyColl = {
data: [1, 3, 8],
forEach(callback) {
this.data.forEach(callback);
},
at(index) {
return this.data.at(index); // target >= ES2022
},
}
coll.at(-1); // 8
Теперь попробуем обобщить этот тип, то есть сделать из него дженерик. Для этого нужно сделать две простые вещи:
- для элементов коллекции вместо
number
написатьT
или любое другое имя, начинающееся с большой буквы - добавить
T
как параметр типа к определению
Так это работает на практике:
type MyColl<T> = {
data: Array<T>;
forEach(callback: (value: T, index: number, array: Array<T>) => void): void;
at(index: number): T | undefined;
}
На такое определение типа можно смотреть как на своеобразное определение функции. Для примера попробуем указать конкретный тип — например, MyColl<string>
. В таком случае T
заменяется на string
внутри определения типа. Причем если внутри типа используются другие дженерики, то они вызывают тип дальше. Другими словами, все это работает как вложенные вызовы функций.
Ограничения дженериков
Дженерики могут иметь ограничения. Например, тип, который передается в дженерик, должен реализовывать какой-то интерфейс. Для этого используется ключевое слово extends
. Допустим, что наш тип MyColl
должен работать только с типами, которые реализуют интерфейс HasId
:
interface HasId {
id: number;
}
type MyColl<T extends HasId> = {
data: Array<T>;
forEach(callback: (value: T, index: number, array: Array<T>) => void): void;
at(index: number): T | undefined;
}
Это позволяет нам использовать тип MyColl
только с типами, которые реализуют интерфейс HasId
. Например, такой код не будет работать:
const coll: MyColl<number> = {
data: [1, 3, 8],
forEach(callback) {
this.data.forEach(callback);
},
at(index) {
return this.data.at(index); // target >= ES2022
},
}
Сами дженерики встречаются повсеместно в коде библиотек и фреймворков. Например, в React типы компонентов оборачиваются в дженерики, чтобы можно было указать типы пропсов. С помощью дженериков можно создавать более универсальные типы, которые могут работать с разными типами данных, что мы и рассмотрим в следующих уроках.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.