Перейти к содержимому
Теория · Дженерики · урок 9 из 10

Обобщённые классы

Параметр типа бывает и у классов. Представьте стопку контейнеров на причале. Кран ставит новый контейнер наверх и снимает тоже верхний. Стопке всё равно, что в контейнерах, но в одной стопке груз одного вида: на ней висит общая табличка «только бананы».

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

Класс с параметром типа

Обобщённый класс объявляет параметр типа после имени класса, и этот параметр виден во всех его полях и методах, кроме статических:

TypeScript
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, если стек пуст.

Теперь посмотрим, что происходит при запуске:

  1. new Stack<string>() создаёт стек и подставляет вместо T тип string. Для этого объекта push принимает строку, а pop возвращает string | undefined.
  2. Два вызова push кладут "a", затем "b".
  3. Первый pop снимает "b", второй — "a", а третий находит стек пустым и возвращает undefined. Программа печатает b a undefined.

Вот почему в типе результата pop есть undefined. TypeScript не считает вызовы push и не знает, пуст ли стек сейчас.

Аргумент типа задаёт табличку на стопке. Если положить в words число, tsc его не пропустит: words.push(5) даёт ошибку.

TEXT
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 тоже обобщённые:

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

Разберём, что здесь происходит:

  1. Map<K, V> берёт два параметра типа: K для ключей и V для значений. В new Map<string, number>() ключи — строки, значения — числа.
  2. get объявлен как get(key: K): V | undefined, поэтому у age тип number | undefined: ключа может и не быть.
  3. Set<T> хранит значения одного типа. В new Set(["ts", "js"]) аргумент типа не написан, и TypeScript выводит T из аргумента конструктора, как у функции: получается Set<string>.

Так же устроен Promise<T> — промис, который в итоге даст значение типа T. Например, Promise<string> обещает строку.

Здесь аналогия со стопкой заканчивается. Табличку на настоящей стопке можно снять и повесить другую. А тип объекта задаётся при создании и больше не меняется: words навсегда остаётся Stack<string>.

Частая ошибка

Объект обобщённого класса создали без аргумента типа:

TypeScript
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()); // ✗
TEXT
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.