Перейти к содержимому
Теория · Классы · урок 9 из 12

Наследование

Архитектору заказали дом с гаражом. Рисовать всё заново он не станет: возьмёт готовый чертёж дома и допишет к нему гараж. Всё, что было в старом чертеже, — комнаты, окна, кухня — достанется новому без изменений. Если какую-то комнату нужно переделать, архитектор отметит её на новом чертеже.

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

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

Разберём пример по строкам:

  1. У Animal есть поле nick и конструктор, который его заполняет.
  2. class Dog extends Animal получает всё это и добавляет метод bark.
  3. Своего конструктора у Dog нет, поэтому new Dog("Rex") вызывает конструктор Animal.
  4. Программа печатает Rex: woof.

Раз конструктор достался от Animal, tsc сверяет аргументы с ним. Вызов new Dog() без имени не пройдёт:

TEXT
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 перед методом говорит, что это замена:

TypeScript
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, ANNA
  1. override greet заменяет метод greet из Greeter. Типы параметров и результата должны подходить к методу базового класса.
  2. super.greet(who) вызывает исходный метод Greeter, и LoudGreeter переводит его результат в верхний регистр.
  3. Переменная g имеет тип Greeter, а лежит в ней объект LoudGreeter.
  4. Программа печатает HELLO, ANNA.

override защищает от опечаток. Если написать override gret(…), tsc заметит, что в базовом классе такого метода нет:

TEXT
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:

TypeScript
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);
    }
}
TEXT
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 проверяет, что такой метод есть в базовом классе.
  • Объект подкласса подходит к типу базового класса, а вызывается метод самого объекта.