abstract и implements
В архитектурном бюро есть типовой проект дома. В нём всё начерчено, кроме одной комнаты: на её месте надпись «спроектируйте сами». Строить по такому проекту нельзя, инспектор не примет дом с пустым местом. Сначала архитектор дорисовывает комнату, и только по готовому чертежу строят дом.
У инспектора есть и другой документ — список требований: «в доме должен быть выход на крышу». Он сверяет с этим списком готовый чертёж, но сам список ничего не рисует. Каждый пункт архитектор чертит сам.
В TypeScript типовой проект — это абстрактный класс, а список требований — интерфейс в implements.
Абстрактный класс
Абстрактный класс — класс с ключевым словом abstract, по которому нельзя создать объект. Он может оставить методы без тела, и тогда каждый подкласс обязан их написать.
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Разберём пример по строкам:
abstract class Shape— типовой проект: создать объектShapeнельзя.abstract area(): number;— метод без тела, та самая пустая комната. Тип результата уже задан.describe— готовый метод, его получит каждый подкласс. Он вызываетarea, хотя её тела пока нет.Squareдописываетarea, и по нему уже можно создать объект.- Программа печатает
area 4.0.
Две ошибки, которые ловит tsc. Вызов new Shape():
error TS2511: Cannot create an instance of an abstract class.Подкласс, который не написал area, например class Circle extends Shape {}:
error TS2515: Non-abstract class 'Circle' does not implement inherited abstract member area from class 'Shape'.implements
Ключевое слово implements после имени класса сверяет класс с интерфейсом: всё, что требует интерфейс, должно быть в классе.
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: 100Printableтребует методprint, который возвращает строку.class Invoice implements Printableобещает, что такой метод в классе есть, иtscэто проверяет.- Поле
totalинтерфейс не требует, и оно ему не мешает. - Программа печатает
Total: 100.
Если убрать из Invoice метод print, tsc сообщит об ошибке, и вторая строка сообщения назовёт, чего не хватает:
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 передаст классу типы параметров:
interface Printable {
print(copies: number): string;
}
class Invoice implements Printable {
print(copies) { // ✗
return `${copies} copies`;
}
}error TS7006: Parameter 'copies' implicitly has an 'any' type.Интерфейс знает, что copies — число, но implements ничего не передаёт в класс, даже типы. У параметра без типа тип any, а строгий режим этого не разрешает. Напишите тип в самом методе: print(copies: number): string.
Главное
- По абстрактному классу нельзя создать объект (
TS2511), а егоabstract-методы обязан написать каждый подкласс (TS2515). implementsпроверяет, что в классе есть всё, что требует интерфейс, иначе будет ошибкаTS2420.implementsничего не добавляет в класс, даже типы параметров, и в JavaScript от него ничего не остаётся.- Базовый класс у класса один, а интерфейсов в
implementsможет быть сколько угодно.