TypeScript, "надмножество 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 не выдает ошибку.
Завтра состоится открытое занятие «Обзор технологий для построения API», на котором рассмотрим несколько протоколов клиент-серверных приложений. На примере увидим сильные стороны и недостатки. Регистрация для всех желающих здесь.
Комментарии (3)
kahi4
31.08.2022 17:58Позанудствую:
По-русски это называется «обобщенное программирование» и «обобщенные типы»
Им можно задавать значение по-умолчанию (через =), считается хорошей практикой «заземлять» все обобщенные типы на unknown, за исключением тривиальных типов, а то ts будет их радостно выводить в any при первой же возможности
«Как мы видим, если тип не передан в
<>
, то он выводится автоматически. Вывод типа делает код короче, но в сложных определениях может потребоваться явная передача типа.». TS не ленивый и КАКОЙ-ТО тип выведет всегда. Передавать нужно либо для сужения, либо для расширения типа (либо если в цепочке есть рак в виде any). Например:
const personArray = convertPersonToArray<Person>({ name: foo }) // personArray: [Person] // без явного приведения будет // personArray: [{ name: string }] // но это совсем простой пример и так же радостно можно сделать обычное приведение типов
К слову, в таком тривиальном случае можно не указывать возврат функции тоже.
Классы:
В getFormattedData при наследовании можно и не указывать возвращаемый тип, покуда он определен в родителе
Стоит заметить, что у наследника могут быть свои обобщенные типы, но он должен указать все типы у родителя. Т.е.:
// работает class ContactLocalDatabase<T> extends BaseLocalDataBase<ContactTable, T> {} // уже нет class ContactLocalDatabase extends BaseLocalDataBase<ContactTable> {} // not enough generic types // заметьте, что если родитель имел бы второй тип со значением по-умолчанию, тогда бы сработало
Не стоит забывать о существовании перегрузки функций и об арифметических типах, часто они гораздо уместнее обобщенных типов.
Surof1n
02.09.2022 13:50Поменяйте в примерах функции конструкторы на примитивные типы: String — string
aamonster
Для плюсовиков можете сразу напомнить разницу между темплейтами и дженериками: дженерик служит только для контроля типов, с any будет работать точно так же...