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

abstract и implements

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

У инспектора есть и другой документ — список требований: «в доме должен быть выход на крышу». Он сверяет с этим списком готовый чертёж, но сам список ничего не рисует. Каждый пункт архитектор чертит сам.

В TypeScript типовой проект — это абстрактный класс, а список требований — интерфейс в implements.

Абстрактный класс

Абстрактный класс — класс с ключевым словом abstract, по которому нельзя создать объект. Он может оставить методы без тела, и тогда каждый подкласс обязан их написать.

TypeScript
abstract class Shape {
    abstract area(): number;
    describe(): string {
        return `area ${this.area().toFixed(1)}`;
    }
}

class Square extends Shape {
    side = 2;
    override area(): number {
        return this.side ** 2;
    }
}

console.log(new Square().describe()); // area 4.0

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

  1. abstract class Shape — типовой проект: создать объект Shape нельзя.
  2. abstract area(): number; — метод без тела, та самая пустая комната. Тип результата уже задан.
  3. describe — готовый метод, его получит каждый подкласс. Он вызывает area, хотя её тела пока нет.
  4. Square дописывает area, и по нему уже можно создать объект.
  5. Программа печатает area 4.0.

Две ошибки, которые ловит tsc. Вызов new Shape():

TEXT
error TS2511: Cannot create an instance of an abstract class.

Подкласс, который не написал area, например class Circle extends Shape {}:

TEXT
error TS2515: Non-abstract class 'Circle' does not implement inherited abstract member area from class 'Shape'.

implements

Ключевое слово implements после имени класса сверяет класс с интерфейсом: всё, что требует интерфейс, должно быть в классе.

TypeScript
interface Printable {
    print(): string;
}

class Invoice implements Printable {
    total = 100;

    print(): string {
        return `Total: ${this.total}`;
    }
}

const doc: Printable = new Invoice();
console.log(doc.print()); // Total: 100
  1. Printable требует метод print, который возвращает строку.
  2. class Invoice implements Printable обещает, что такой метод в классе есть, и tsc это проверяет.
  3. Поле total интерфейс не требует, и оно ему не мешает.
  4. Программа печатает Total: 100.

Если убрать из Invoice метод print, tsc сообщит об ошибке, и вторая строка сообщения назовёт, чего не хватает:

TEXT
error TS2420: Class 'Invoice' incorrectly implements interface 'Printable'.
  Property 'print' is missing in type 'Invoice' but required in type 'Printable'.

Базовый класс через extends у класса может быть только один, а интерфейсов в implements — сколько угодно, через запятую: class Invoice implements Printable, Payable.

implements ничего не даёт

implements только проверяет. Он не добавляет в класс ни одного поля и ни одного метода, а в JavaScript от implements Printable не остаётся ни слова.

Здесь картинка с инспектором перестаёт работать. Инспектор сверяет чертёж только со списком, который к нему приложен, а TypeScript и без implements примет Invoice везде, где ждут Printable: он сравнивает форму. implements нужен, чтобы ошибку показали в самом классе, а не там, где его объект передают дальше.

От слова abstract в JavaScript тоже ничего не остаётся: Shape там обычный класс, а метода area в нём нет вовсе. Создать Shape не даёт только tsc.

На собеседовании часто спрашивают, чем абстрактный класс отличается от интерфейса. Ответ:

  • абстрактный класс может нести готовый код и поля, как метод describe, и подкласс получает их через extends;
  • интерфейс только описывает форму и ничего не даёт классу;
  • базовый класс у класса один, а интерфейсов можно реализовать сколько угодно.

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

Ждали, что implements передаст классу типы параметров:

TypeScript
interface Printable {
    print(copies: number): string;
}

class Invoice implements Printable {
    print(copies) { // ✗
        return `${copies} copies`;
    }
}
TEXT
error TS7006: Parameter 'copies' implicitly has an 'any' type.

Интерфейс знает, что copies — число, но implements ничего не передаёт в класс, даже типы. У параметра без типа тип any, а строгий режим этого не разрешает. Напишите тип в самом методе: print(copies: number): string.

Главное

  • По абстрактному классу нельзя создать объект (TS2511), а его abstract-методы обязан написать каждый подкласс (TS2515).
  • implements проверяет, что в классе есть всё, что требует интерфейс, иначе будет ошибка TS2420.
  • implements ничего не добавляет в класс, даже типы параметров, и в JavaScript от него ничего не остаётся.
  • Базовый класс у класса один, а интерфейсов в implements может быть сколько угодно.