Что такое дженерики в TypeScript?
Автор: rikki_tikkiTypeScript, "надмножество JS", облегчает создание поддерживаемых, понятных и масштабируемых приложений благодаря эффективной возможности проверки типов.
Дженерики играют важную роль в TypeScript, поскольку они позволяют нам писать многократно используемый код, принимающий в качестве аргументов как значения, так и типы.
Дженерики в функциях
Дженерики помогают нам сделать код наиболее пригодным для повторного использования. Давайте попробуем понять, что такое дженерики и зачем они нам нужны, на примере ниже
interface Person { name: String } const convertStringToArray = (value: String): Array<String> => { return [value]; } const convertNumberToArray = (value: Number): Array<Number> => { return [value]; } const convertPersonToArray = (value: Person): Array<Person> => { return [value]; }
Обратите внимание, что в приведенном выше фрагменте у нас есть три функции, выполняющие практически одно и то же действие. Это дублирующий код, который так и кричит о необходимости сделать его многоразовым.
Одна вещь, которую мы бы могли сделать, это поместить тип any
, чтобы значения типа String, Number и Person можно было использовать в качестве аргументов в одной и той же функции. К сожалению, это вызывает больше проблем, чем решает (в общем, если вы планируете использовать тип any
очень часто, то, возможно, лучше оставить его исключительно в JS).
Решение "проблемы повторного использования" с помощью дженериков — пример:
export interface Person { name: String; } export const convertToValueArray = <T>(value: T): Array<T> => { return [value]; }; const person: Person = { name: "Mahesh" }; const firstPerson = convertToValueArray(person)[0];
Функция converToValueArray
получает значение выбранного типа <T>
и возвращает массив этого типа: Array<T>
. Так, например, если значение имеет тип String, то возвращаемый тип будет Array<String>
.
Давайте посмотрим, как TypeScript показывает ошибку, когда мы определяем наш дженерик-тип.
Обратите внимание, в строке 18, после использования дженериков, если мы хотим получить доступ к age (возрасту), он показывает соответствующую ошибку, что нам и нужно для получения своевременного фидбека на любую неточность.
Выведенный тип
Давайте определим функцию, которая принимает дженерик-тип.
function convertToArray<T>(args: T): Array<T> { return [args]; }
Мы можем вызвать функцию двумя способами
convertToArray("someString"); convertToArray<String>("someString");
Как мы видим, если тип не передан в <>
, то он выводится автоматически. Вывод типа делает код короче, но в сложных определениях может потребоваться явная передача типа.
Более одного дженерик-типа
Точно так же, как аргументы функции, мы можем передавать более одного типа, что не зависит от количества аргументов функции. Например
function doStuff<T, U>(name: T): T { // ...some process return name; }
Вышеуказанная функция может быть вызвана следующим образом:
doStuff<String, Number>("someString");
Дженерик-классы
Много раз нам требовалось бы создать дженерик-класс, например, абстрактные Base (базовые) классы. В них мы можем передавать типы при создании экземпляров классов.
interface DatabaseConnector { get: Function; put: Function; } abstract class BaseLocalDatabase<T, M> { tableName: String; databaseInstance: DatabaseConnector; constructor(tableName: String) { this.tableName = tableName; this.databaseInstance = getDatabase<T>(tableName); } async insert(data: M): Promise<void> { await this.databaseInstance.put(data); } async get(id: Number): Promise<M> { return await this.databaseInstance.get(id); } abstract getFormattedData(): Array<M>; }
Как видно, в строке 6 мы создали базовый локальный класс базы данных, который мы можем использовать для создания экземпляра одной конкретной таблицы и выполнения операций над экземпляром базы данных. Давайте напишем класс contact
, который расширяет этот базовый класс, чтобы он мог наследовать некоторые свойства от родителя.
interface ContactTable { name: String; } interface ContactModel { id: String; name: String; phoneNumber: String; profilePicture: String; createdAt: Date; updatedAt: Date; } class ContactLocalDatabase extends BaseLocalDataBase<ContactTable, ContactModel> { // overriden function getFormattedData(): ContactModel[] { // format and return data } }
- В строке 14, при расширении дженерик-класса, в данной базе данных мы должны передать два типа. В нашем случае
ContactTable
иContactModel
. - Строка 17: База данных
ContactLocalDatabase
получит функции из родительского класса и должна переопределить функциюgetFormattedData
, поскольку она определена как абстрактная функция в родительском базовом классе. - Строка 17: Теперь это функция, имеющая дженерик-тип, о котором мы говорили в первом разделе.
Давайте создадим экземпляр ContactLocalDataBase
, чтобы увидеть дженерики класса в действии.
let contactsLocalDatabase = new ContactLocalDatabase("Contact table"); await contactsLocalDatabase.get(21); const contactModel: ContactTable = { id: 12, name: 'Some name', ..., // define all other values. ... } await contactsLocalDatabase.insert(contactModel); const contactArray: Array<ContactModel> = contactsLocalDatabase.getFormattedData();
- Строка 1: поскольку мы определили типы класса
ContactLocalDatabase
при использовании ключевого слова new, типы не нужно передавать в базовый класс. - Строки 3, 11, 13: мы можем заметить, что эти функции принадлежат абстрактному классу. Они ведут себя в соответствии с определениями дженерик-класса.
Ограничения дженериков
До сих пор было понятно, что дженерики — это способ написания кода таким образом, что наш код может поддерживать более одного типа и тип может быть передан в качестве параметра.
Исходя из этого, передаваемый тип может быть любым предопределенным или сложным пользовательским типом.
Иногда, если мы хотим получить доступ к какой-либо функции из дженерик-типизированной переменной, это приведет к ошибке.
- Строка 6: Это вполне допустимая ошибка, так как у данных дженерик-тип. Они могут быть String, Number, Float или любым другим типом, а передаваемые данные могут как учитывать длину, так и нет.
Следовательно, мы можем добавить некоторые ограничения к любым дженерикам, где TS будет гарантировать, что только те значения могут быть переданы в функцию или класс, которые выполняют ограничивающее условие. Давайте добавим некоторые ограничения к определению нашей дженерик-функции.
- Строка 4: мы расширили тип
T
, чтобы он имел свойство length (длина); - Строка 5: исчезла ошибка, которая говорила, что свойство length не существует для типа 'T';
- Строка 10: когда мы вызываем функцию с числом, она выдает ошибку, объясняя, что не выполняется условие ограничения;
- Строки 12 и 13: когда мы передаем корректные данные, такие как String или Array, TS не выдает ошибку.