Стандартные дженерики (Generic Type)

Массивы: T[] vs Array<T>

В предыдущем разделе мы узнали, что массивы в тайпскрипте можно задавать через квадратные скобоки. Например, number[] или Person[].

На самом деле это сокращенная форма записи обобщенного типа (generic type) Array. Как задавать свои дженерики, мы узнаем позже. Сейчас мы хотим научиться их читать и использовать.

В примере ниже primeNumbers1 и primeNumbers2 — переменные одного типа.

const primeNumbers1: number[] = [2, 3, 5, 7, 11]; const primeNumbers2: Array<number> = [2, 3, 5, 7, 11];

Нельзя объявить переменную типа Array (вообще массив). Это должен быть массив чего-то: Array<number>, Array<Person>, Array<number | string> или даже Array<Array<string>>.

const keyboard: Array<Array<string>> = [ ["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"], ["A", "S", "D", "F", "G", "H", "J", "K", "L"], ["Z", "X", "C", "V", "B", "N", "M"], ];

Разумеется, тип keyboard мог бы быть описан как string[][]. В рабочих задачах я всегда использую вариант с квадратными скобками. Запись через Array тут нужна скорее, чтобы подготовить вас к другим дженерикам, о которых мы сейчас и поговорим.

Название

Почему вообще используется слово дженерик?

На самом деле полностью термин называется Generic Type (по-русски — обобщенный тип). Имеется в виду, что есть, например, тип Set вообще — он обобщенный. А есть тип Set<number> или Set<User> — это конкретные типы. И мы, как бы подставляя обычный тип в обобщенный, как значение в функцию, получаем конкретный тип.

Дальше произошло то, что в лингвистике называется эллиптическая субстантивация: в словосочетании без потери смысла опускается существительное.

Например, вы говорите по-русски: «У меня есть высшее», имея в виду высшее образование. Или говорите: «Я не буду первое», имея в виду, что не хотите суп (первое блюдо). Или что будете работать в выходной (выходной день, естественно).

В английском мы говорим a convertible вместо a convertible car или the elderly вместо elderly people. В программировании constant value превращается в constant, a primitive type в a primitive, а а generic type — в a generic.

Походая ситуация произошла в медицине. Препараты-дженерики протипоставляются препаратам-брендам. В них то же самое действующее вещество, но производитель не платит за использование торговой марки. Пример использования: The biggest difference between the brand-name drugs and the generics is price.

Set<T>

По аналогии с массивом мы можем объявить множество.

const marks: Array<number> = []; const uniqueMarks: Set<number> = new Set();

Если мы не укажем тип Set<number>, тайпскрпит сам определит тип. И тип этот будет Set<unknown>. В этом случае мы сможем добавлять в сет что угодно, потому что unknown обозначает любое значение. Вряд ли нам это нужно, потому что мы стремимся к тому, чтобы тайпскрпит указывал на ошибку, если мы добавляем что-то не то.

В примере выше слово Set используется в двух местах. После двоеточия как тайпскриптовый тип, после new — как джаваскриптовый класс. В этом нет никакой проблемы. Тайпскрипт по местополождению определяет значение этого слова.

При этом есть возможность записать тип сета при инциализации сета короче:

const uniqueMarks = new Set<number>();

Мы могли бы так же поступить и с массивом, записав new Array<number>:

const marks1 = new Array<number>(); const marks2: number[] = [];

Просто на практике в этом нет необходимости, потому что у массива есть и более короткий способ объявления типа (через тип и квадратные скобки) и литерал для инциализации (пустые или не пустые квадратные скобки).

Iterable<T>

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

Предположим, я хочу проверить, что в моем массиве чесел или множестве чисел все элементы положительные. Тогда функция может принимать юнион массива и сета.

function allPositive(numbers: number[] | Set<number>): boolean { for(const number of numbers) { if (number < 0) { return false; } } return true; }

А можно описать еще более общий дженерик интерфейс Iterable, так как все равно в решении мы пользуемся циклом for of.

function allPositive(numbers: Iterable<number>): boolean { for(const number of numbers) { if (number < 0) { return false; } } return true; } const array: number[] = [1, 2, 3]; const set = new Set(array); const iterator = array.keys(); allPositive(array); allPositive(set); allPositive(iterator);

Map<K, V>

В отличие от Array и Set, хранящих значения, Map описывает структуру, в которой есть и ключи, и значения. Соответственно, у Map мы должны указывать два типа.

const number2prime = new Map<number, boolean>([ [1, false], [2, true], [3, true], [4, false], [5, true], ]);

Метод has в этом случае может принимать только число:

number2prime.has(5); // ok number2prime.has("qwe"); // error

Метод get возвращает boolean | undefined:

const isFivePrime = number2prime.get(5); // boolean | undefined

Если мы уверены, что там не undefined (например, только что это проверили с помощью метода has), можно воспользоваться постфиксным оператором !. Официальное название Non-null assertion operator, но иногда его называют exclamation mark или bang operator. Он убирает из типа значения null и undefined. Используйте его аккуратно, тайпскрипт не зря говорит, что может вернуться undefined.

if (number2prime.has(5)) { const isFivePrime = number2prime.get(5)!; // boolean }

Promise<T>

Так как класс Promise является оберткой над значением, то и тип Promise является дженерик типом. Какого типа значение лежит внутри промиса, таким типом и параметризируется интерфейс.

const p1: Promise<number> = Promise.resolve(1); const p2: Promise<string> = new Promise(resolve => resolve("hello")); const p3 = new Promise<number[]>(resolve => resolve([]));

Было бы здорово, если бы мы могли параметризовать промис типами на случай fulfilled и на случай rejected. Например, Promise<string, Error> означал бы, что промис успешно резолвится строкой, а в случае неудачи реджектися ошибкой. Или Promise<number[], string> означал бы, что в случае успеха внутри промиса находится массив чисел, а причиной реджекта является строка.

Проблема в том, что в тайпскрипте в целом не типизируется поведение, связанное со throw. Поэтому, например, в этом случае мы бы не смогли вывести, чем реджектится промис p2:

function foo(): void { throw "some string"; } const p1: Promise<number, never> = Promise.resolve(1); const p2 = p1.then(() => foo());

На уровне типов мы определяем, что foo ничего не возвращает, но не можем указать, что она бросает строку. Соотвественно, тайпскрипт не сможет выводить тип p2.

Record<K, V>

Record достаточно важный, чтобы посвятить ему отдельный раздел.

Другие стандартные дженерики

Намного реже встречаются WeakMap, WeakSet, Generator, AsyncGenerator, Iterator, AsyncIterator, AsyncIterable. В целом для них всё работает аналогично.

Задачи на TypeScript

Теория по TypeScript