Обобщённые классы
Параметр типа бывает и у классов. Представьте стопку контейнеров на причале. Кран ставит новый контейнер наверх и снимает тоже верхний. Стопке всё равно, что в контейнерах, но в одной стопке груз одного вида: на ней висит общая табличка «только бананы».
В программах такая стопка называется стеком: последний положенный элемент снимают первым. Стек чисел и стек строк устроены одинаково, отличается только тип элементов. Значит, у класса стека должен быть параметр типа.
Класс с параметром типа
Обобщённый класс объявляет параметр типа после имени класса, и этот параметр виден во всех его полях и методах, кроме статических:
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const words = new Stack<string>();
words.push("a");
words.push("b");
console.log(words.pop(), words.pop(), words.pop()); // b a undefinedРазберём класс по частям:
class Stack<T>объявляет параметр типаTдля всего класса;items: T[]— закрытый массив, в котором лежат элементы типаT;push(item: T)принимает только значения типаT;pop(): T | undefinedвозвращает верхний элемент илиundefined, если стек пуст.
Теперь посмотрим, что происходит при запуске:
new Stack<string>()создаёт стек и подставляет вместоTтипstring. Для этого объектаpushпринимает строку, аpopвозвращаетstring | undefined.- Два вызова
pushкладут"a", затем"b". - Первый
popснимает"b", второй —"a", а третий находит стек пустым и возвращаетundefined. Программа печатаетb a undefined.
Вот почему в типе результата pop есть undefined. TypeScript не считает вызовы push и не знает, пуст ли стек сейчас.
Аргумент типа задаёт табличку на стопке. Если положить в words число, tsc его не пропустит: words.push(5) даёт ошибку.
error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.Классическая задача на собеседовании — написать типизированный стек или очередь: class Stack<T> с методами push и pop. Главное в ответе — один параметр типа на весь класс и T | undefined у pop.
Map, Set и Promise
Многие встроенные классы JavaScript в TypeScript тоже обобщённые:
const ages = new Map<string, number>();
ages.set("ann", 30);
const age = ages.get("ann");
console.log(age); // 30
const tags = new Set(["ts", "js"]);
console.log(tags.has("ts")); // trueРазберём, что здесь происходит:
Map<K, V>берёт два параметра типа:Kдля ключей иVдля значений. Вnew Map<string, number>()ключи — строки, значения — числа.getобъявлен какget(key: K): V | undefined, поэтому уageтипnumber | undefined: ключа может и не быть.Set<T>хранит значения одного типа. Вnew Set(["ts", "js"])аргумент типа не написан, и TypeScript выводитTиз аргумента конструктора, как у функции: получаетсяSet<string>.
Так же устроен Promise<T> — промис, который в итоге даст значение типа T. Например, Promise<string> обещает строку.
Здесь аналогия со стопкой заканчивается. Табличку на настоящей стопке можно снять и повесить другую. А тип объекта задаётся при создании и больше не меняется: words навсегда остаётся Stack<string>.
Частая ошибка
Объект обобщённого класса создали без аргумента типа:
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
const words = new Stack();
words.push("a");
const last = words.pop();
console.log(last.toUpperCase()); // ✗error TS18046: 'last' is of type 'unknown'.У new Stack() нет ни аргумента типа, ни аргументов конструктора, а у words нет аннотации. Поэтому T становится unknown, как у функции, которой не из чего вывести тип. В такую стопку можно положить что угодно, и строку, и число, а вот воспользоваться снятым элементом без проверки нельзя. Напишите аргумент типа: new Stack<string>().
Главное
- Обобщённый класс объявляет параметр типа после имени:
class Stack<T>, иTвиден во всех полях и методах, кроме статических. - Аргумент типа задают при создании объекта: в
new Stack<string>()методpushпринимает только строки, аpopвозвращаетstring | undefined. Map<K, V>,Set<T>иPromise<T>— тоже обобщённые классы; вnew Set(["ts", "js"])Tвыводится из аргумента конструктора, как у функции.- Без аргумента типа, аргументов конструктора и аннотации переменной
Tстановитсяunknown.