Статьи

TypeScript для начинающих, часть 5: Дженерики

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

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

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

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

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

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

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

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

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

01
02
03
04
05
06
07
08
09
10
function randomElem(theArray: any[]): any {
    let randomIndex = Math.floor(Math.random()*theArray.length);
    return theArray[randomIndex];
}
 
let positions = [103, 458, 472, 458];
let randomPosition = randomElem(positions);
 
let colors = [‘violet’, ‘indigo’, ‘blue’, ‘green’];
let randomColor = randomElem(colors);

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

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

1
2
3
// This code will compile without an error.
let colors: string[] = [‘violet’, ‘indigo’, ‘blue’, ‘green’];
let randomColor: number = randomElem(colors);

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

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

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

Обобщенная ошибка TypeScript

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

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

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

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

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

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

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

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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface People {
    name: string
}
 
interface Family {
    name: string,
    age: number,
    relation: string
}
 
interface Celebrity extends People {
    profession: string
}
 
function printName<T extends People>(theInput: T): void {
    console.log(`My name is ${theInput.name}`);
}
 
let serena: Celebrity = {
    name: ‘Serena Williams’,
    profession: ‘Tennis Player’
}
 
printName(serena);

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

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

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

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