Наследование
Архитектору заказали дом с гаражом. Рисовать всё заново он не станет: возьмёт готовый чертёж дома и допишет к нему гараж. Всё, что было в старом чертеже, — комнаты, окна, кухня — достанется новому без изменений. Если какую-то комнату нужно переделать, архитектор отметит её на новом чертеже.
В JavaScript так работает extends: подкласс получает поля и методы базового класса и добавляет свои. TypeScript сохраняет этот механизм и проверяет его: аргументы конструктора, доступ к полям и то, что переделанный метод действительно есть в базовом классе.
class Animal {
protected nick: string;
constructor(nick: string) {
this.nick = nick;
}
}
class Dog extends Animal {
bark(): string {
return `${this.nick}: woof`;
}
}
console.log(new Dog("Rex").bark()); // Rex: woofРазберём пример по строкам:
- У
Animalесть полеnickи конструктор, который его заполняет. class Dog extends Animalполучает всё это и добавляет методbark.- Своего конструктора у
Dogнет, поэтомуnew Dog("Rex")вызывает конструкторAnimal. - Программа печатает
Rex: woof.
Раз конструктор достался от Animal, tsc сверяет аргументы с ним. Вызов new Dog() без имени не пройдёт:
error TS2554: Expected 1 arguments, but got 0.protected в подклассе
Поле nick объявлено как protected. Метод bark — код подкласса, поэтому this.nick ему доступен. А снаружи, в строке new Dog("Rex").nick, tsc сообщит об ошибке TS2445: поле protected видно только классу и его подклассам. Будь поле private, его не увидел бы и bark.
Свой конструктор и super
Когда у подкласса есть собственный конструктор, у него появляются обязанности:
- вызвать
super(…)— конструктор базового класса, с его аргументами;tscпроверит их так же, как уnew; - сделать это до первого обращения к
this: пока базовая часть объекта не построена, писать в него нельзя; - не забыть вызов совсем: без
super(…)будет ошибкаTS2377.
override
Подкласс может заменить метод базового класса своим — такую замену называют переопределением. Ключевое слово override перед методом говорит, что это замена:
class Greeter {
greet(who: string): string {
return `Hello, ${who}`;
}
}
class LoudGreeter extends Greeter {
override greet(who: string): string {
return super.greet(who).toUpperCase();
}
}
const g: Greeter = new LoudGreeter();
console.log(g.greet("Anna")); // HELLO, ANNAoverride greetзаменяет методgreetизGreeter. Типы параметров и результата должны подходить к методу базового класса.super.greet(who)вызывает исходный методGreeter, иLoudGreeterпереводит его результат в верхний регистр.- Переменная
gимеет типGreeter, а лежит в ней объектLoudGreeter. - Программа печатает
HELLO, ANNA.
override защищает от опечаток. Если написать override gret(…), tsc заметит, что в базовом классе такого метода нет:
error TS4117: This member cannot have an 'override' modifier because it is not declared in the base class 'Greeter'. Did you mean 'greet'?Без override такая опечатка прошла бы молча: в классе появился бы новый метод gret, а greet остался бы старым. По умолчанию override писать не обязательно. Флаг --noImplicitOverride делает его обязательным: тогда tsc сообщит о каждой замене без override.
Подкласс подходит к типу базового
В примере выше объект LoudGreeter лежит в переменной типа Greeter, и tsc это разрешает. Здесь аналогия с чертежом перестаёт работать: архитектор может снести комнату из старого чертежа, а подкласс ничего не может убрать. Всё, что есть у Greeter, есть и у LoudGreeter, поэтому его объект подходит везде, где ждут Greeter.
Какой метод вызовет g.greet("Anna")? Метод самого объекта, то есть LoudGreeter. Тип переменной решает другое: что tsc разрешит вызвать. Через g доступны только члены Greeter, и новый метод подкласса через неё вызвать не получится — будет ошибка TS2339.
На собеседовании этот вопрос задают часто: метод какого класса выполнится, если объект подкласса лежит в переменной базового типа. Ответ: метод подкласса, потому что метод выбирает объект, а не тип переменной.
Частая ошибка
В конструкторе подкласса записали своё поле раньше, чем вызвали super:
class Animal {
protected nick: string;
constructor(nick: string) {
this.nick = nick;
}
}
class Dog extends Animal {
breed: string;
constructor(nick: string, breed: string) {
this.breed = breed; // ✗
super(nick);
}
}error TS17009: 'super' must be called before accessing 'this' in the constructor of a derived class.«super нужно вызвать до обращения к this в конструкторе подкласса». JavaScript тоже не даст так сделать и при запуске бросит ReferenceError. Поменяйте две строки местами: сначала super(nick), потом this.breed = breed.
Главное
extendsпередаёт подклассу поля и методы, аtscсверяет аргументыnewс конструктором, который подкласс получил.- Собственный конструктор подкласса вызывает
super(…)до первогоthis. overrideотмечает замену метода, иtscпроверяет, что такой метод есть в базовом классе.- Объект подкласса подходит к типу базового класса, а вызывается метод самого объекта.