В предыдущем разделе мы узнали, что массивы в тайпскрипте можно задавать через квадратные скобоки. Например, 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.
По аналогии с массивом мы можем объявить множество.
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[] = [];
Просто на практике в этом нет необходимости, потому что у массива есть и более короткий способ объявления типа (через тип и квадратные скобки) и литерал для инциализации (пустые или не пустые квадратные скобки).
Иногда мы хотим описать чуть более общую функцию, чем просто для массива или для сета. Например, хочу проверять, что все элементы положительные.
Предположим, я хочу проверить, что в моем массиве чесел или множестве чисел все элементы положительные. Тогда функция может принимать юнион массива и сета.
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);
В отличие от 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
является оберткой над значением, то и тип 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 достаточно важный, чтобы посвятить ему отдельный раздел.
Намного реже встречаются WeakMap, WeakSet, Generator, AsyncGenerator, Iterator, AsyncIterator, AsyncIterable. В целом для них всё работает аналогично.