Advertisement
  1. Code
  2. JavaScript

ТипScript для начинающих, часть 5: Generics

Scroll to top
Read Time: 7 min
This post is part of a series called TypeScript for Beginners.
TypeScript for Beginners, Part 4: Classes

() translation by (you can also view the original English article)

Второй учебник в нашей серии TypeScript для начинающих посвящен основным типам данных, доступных в TypeScript. Проверка типов в TypeScript позволяет нам убедиться, что переменные в нашем коде могут иметь только определенные типы значений, назначенные им. Таким образом, мы можем избежать множества ошибок при написании кода, потому что IDE сможет рассказать нам, когда мы выполняем операцию над типом, который ее не поддерживает. Это делает проверку типов одной из лучших функций TypeScript.

В этом уроке мы остановимся на еще одной важной особенности этого языка-дженерики. С помощью дженериков TypeScript позволяет вам писать код, который может работать с различными типами данных, а не ограничиваться одним. Вы узнаете о необходимости создания дженериков в деталях и о том, как это лучше, чем просто использовать тип данных any, доступный в TypeScript.

Потребность в генериках

Если вы не знакомы с дженериками, вам может быть интересно, зачем они вообще нужны. В этом разделе я отвечу на этот вопрос. Начнем с написания функции, которая вернет случайный элемент из массива чисел.

1
function randomIntElem(theArray: number[]): number {
2
     let randomIndex = Math.floor(Math.random()*theArray.length);
3
     return theArray[randomIndex];
4
 }
5
 
6
 let positions: number[] = [103, 458, 472, 458];
7
 let randomPosition: number = randomIntElem(positions);

Функция randomElem, которую мы только что определили, принимает в качестве единственного параметра массив чисел. Тип возврата функции также задан как число. Мы используем функцию Math.random() для возврата случайного числа с плавающей запятой между 0 и 1. Умножая его на длину заданного массива и вызывая Math.floor(), результат дает нам случайный индекс. Как только у нас есть случайный индекс, мы возвращаем элемент в этом конкретном индексе.

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

1
function randomStrElem(theArray: string[]): string {
2
     let randomIndex = Math.floor(Math.random()*theArray.length);
3
     return theArray[randomIndex];
4
 }
5
 
6
 let colors: string[] = ['violet', 'indigo', 'blue', 'green'];
7
 let randomColor: string = randomStrElem(colors);

Что делать, если вам нужно выбрать случайный элемент из массива интерфейса, который вы определили? Создание новой функции каждый раз, когда вы хотите получить случайный элемент из массива различных объектов, невозможно.

Одним из решений этой проблемы является установка типа параметра массива, передаваемого в функции, как any[]. Таким образом, вы можете просто написать свою функцию только один раз, и она будет работать с массивом всех типов.

1
function randomElem(theArray: any[]): any {
2
     let randomIndex = Math.floor(Math.random()*theArray.length);
3
     return theArray[randomIndex];
4
 }
5
 
6
 let positions = [103, 458, 472, 458];
7
 let randomPosition = randomElem(positions);
8
 
9
 let colors = ['violet', 'indigo', 'blue', 'green'];
10
 let randomColor = randomElem(colors);

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

Раньше мы были уверены, что randomPosition будет числом, а randomColor - строкой. Это помогло нам соответствующим образом использовать эти значения. Теперь все, что мы знаем, это то, что возвращаемый элемент может быть любого типа. В приведенном выше коде мы можем указать тип randomColor как number и не получить никакой ошибки.

1
// This code will compile without an error.

2
 let colors: string[] = ['violet', 'indigo', 'blue', 'green'];
3
 let randomColor: number = randomElem(colors);

Лучшим решением для избежания дублирования кода при сохранении информации о типе является использование дженериков. Вот общая функция, которая возвращает случайные элементы из массива.

1
function randomElem<T>(theArray: T[]): T {
2
     let randomIndex = Math.floor(Math.random()*theArray.length);
3
     return theArray[randomIndex];
4
 }
5
 
6
 let colors: string[] = ['violet', 'indigo', 'blue', 'green'];
7
 let randomColor: string = randomElem(colors);

Теперь я получу ошибку, если попытаюсь изменить тип randomColor от string к number. Это доказывает, что использование дженериков намного безопаснее, чем использование типа any в таких ситуациях.

TypeScript Generics ErrorTypeScript Generics ErrorTypeScript Generics Error

Использование дженериков может показаться очень ограниченным

В предыдущем разделе обсуждалось, как вы можете использовать генерики вместо типа any, чтобы написать одну функцию и не жертвовать преимуществами проверки типов. Одна из проблем с общей функцией, которая была записана в предыдущем разделе, заключается в том, что TypeScript не позволит нам выполнять много операций с переданными ему переменными.

Это связано с тем, что TypeScript не может делать никаких предположений о типе переменной, которая будет передана нашей общей функции заранее. В результате наша общая функция может использовать только те операции, которые применимы ко всем типам данных. Следующий пример должен сделать эту концепцию более понятной.

1
function removeChar(theString: string, theChar: string): string {
2
     let theRegex = new RegExp(theChar, "gi");
3
     return theString.replace(theRegex, '');
4
 }

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

1
function removeIt<T>(theInput: T, theIt: string): T {
2
     let theRegex = new RegExp(theIt, "gi");
3
     return theInput.replace(theRegex, '');
4
 }

Функция removeChar не показала вам ошибку. Однако, если вы используете replace внутри removeIt, TypeScript скажет вам, что для типа T не существует replace. Это связано с тем, что TypeScript больше не может считать, что значение InInput будет строкой.

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

Одна важная вещь, которую вы должны запомнить на этом этапе, заключается в том, что вам обычно не нужно создавать функции, которые будут использоваться со всеми типами данных. Чаще всего создается функция, которая будет использоваться с определенным набором или диапазоном типов данных. Это ограничение на типы данных делает общие функции более полезными.

Создание общих функций с использованием ограничений

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

Вы можете использовать ключевое слово extends, чтобы ограничить типы данных, которые передаются общей функции в TypeScript. Однако extends распространяется только на интерфейсы и классы. Это означает, что большинство общих функций, которые вы создаете, будут иметь параметры, которые расширяют базовый интерфейс или класс.

Вот общая функция, которая печатает имя людей, членов семьи или знаменитостей, переданных ей.

1
interface People {
2
     name: string
3
 }
4
 
5
 interface Family {
6
     name: string,
7
     age: number,
8
     relation: string
9
 }
10
 
11
 interface Celebrity extends People {
12
     profession: string
13
 }
14
 
15
 function printName<T extends People>(theInput: T): void {
16
     console.log(`My name is ${theInput.name}`);
17
 }
18
 
19
 let serena: Celebrity = {
20
     name: 'Serena Williams',
21
     profession: 'Tennis Player'
22
 }
23
 
24
 printName(serena);

В приведенном выше примере мы определили три интерфейса, и каждый из них имеет свойство name. Общая функция printName, которую мы создали, примет любой объект, который расширяет People. Другими словами, вы можете передать объект или объект знаменитости этой функции, и он напечатает свое имя без каких-либо претензий. Вы можете определить еще много интерфейсов, и до тех пор, пока у них есть свойство name, вы сможете использовать функцию printName без каких-либо проблем.

Это был очень простой пример, но вы можете создавать более полезные общие функции, когда вам будет удобнее всего процесса. Например, вы можете создать общую функцию, которая вычисляет общую стоимость различных предметов, проданных за данный месяц, если каждый элемент имеет свойство price для хранения цены и sold, которое хранит количество проданных товаров. Используя generics, вы сможете использовать ту же функцию, пока элементы расширяют один и тот же интерфейс или класс.

Заключение

В этом уроке я попытался осветить основы дженериков в TypeScript в удобной для новичков форме. Мы начали статью, обсуждая необходимость создания дженериков. После этого мы узнали о правильном способе использования дженериков, чтобы избежать дублирования кода, не жертвуя способностью проверки типов. Когда вы поймете основы, обсуждаемые здесь, вы можете прочитать больше о дженериках в официальной документации.

Если у вас есть какие-либо вопросы, связанные с этим руководством, я буду рад ответить на них в комментариях.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.