Что такое abstract class в TypeScript?
Abstract class в TypeScript
Абстрактный класс (abstract class) — это специальный вид класса, который служит базой для других классов, но не может быть создан напрямую с помощью new. Он позволяет описать общий интерфейс и частичную реализацию, которую наследники обязаны дополнить.
Ключевые характеристики
- Нельзя инстанциировать напрямую — попытка вызвать
new AbstractClass()вызовет ошибку компиляции. - Может содержать абстрактные методы — методы с ключевым словом
abstractбез тела; наследник обязан их реализовать. - Может содержать конкретные методы — обычные методы с готовой реализацией, которые наследники могут использовать или переопределить.
- Может содержать поля и конструктор — в отличие от интерфейса, абстрактный класс поддерживает состояние и инициализационную логику.
Абстрактный класс vs интерфейс
Интерфейс описывает только форму объекта и существует только на уровне типов (стирается при компиляции). Абстрактный класс компилируется в реальный JavaScript-класс, может хранить состояние, содержать конструктор и логику, а также использоваться с instanceof.
Выбирайте абстрактный класс, когда нужно:
- Определить общее состояние или конструктор для группы классов.
- Предоставить готовую реализацию части методов (шаблонный метод).
- Выразить отношение «является» (is-a) с частичной логикой.
Выбирайте интерфейс, когда нужно описать только контракт без реализации или когда класс должен реализовать несколько контрактов одновременно.
Паттерн «Шаблонный метод»
Абстрактный класс отлично подходит для паттерна Template Method: базовый класс определяет алгоритм в виде конкретного метода, а шаги алгоритма делегируются абстрактным методам, которые переопределяют наследники.
Важные нюансы
- Абстрактный метод автоматически является
public(или может бытьprotected), но неprivate. - Наследник должен реализовать все абстрактные методы, иначе он тоже должен быть объявлен абстрактным.
- Абстрактные классы поддерживают
generics,access modifiersиreadonly.
Что хочет услышать интервьюер
Понимание того, что abstract class нельзя инстанциировать напрямую и зачем это нужно
Различие между абстрактными методами (без реализации) и конкретными методами (с реализацией) внутри абстрактного класса
Осознанное сравнение abstract class и interface: когда выбрать одно, а когда другое
Знание того, что наследник обязан реализовать все абстрактные методы или сам стать абстрактным
Практический пример применения — например, паттерн Template Method или базовый класс для группы сущностей
Пример: Базовый пример абстрактного класса
abstract class Shape {
// Абстрактный метод — наследник обязан реализовать
abstract getArea(): number;
// Конкретный метод — готовая реализация для всех наследников
describe(): string {
return `Площадь фигуры: ${this.getArea()}`;
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
// Реализуем обязательный абстрактный метод
getArea(): number {
return Math.PI * this.radius ** 2;
}
}
class Rectangle extends Shape {
constructor(private width: number, private height: number) {
super();
}
getArea(): number {
return this.width * this.height;
}
}
// const shape = new Shape(); // Ошибка: Cannot create an instance of an abstract class
const circle = new Circle(5);
console.log(circle.describe()); // Площадь фигуры: 78.53...
Пример: Паттерн Template Method
abstract class DataExporter {
// Шаблонный метод — задаёт алгоритм экспорта
export(data: unknown[]): string {
const filtered = this.filter(data);
const transformed = this.transform(filtered);
return this.serialize(transformed);
}
// Шаги алгоритма — наследники определяют детали
protected abstract filter(data: unknown[]): unknown[];
protected abstract transform(data: unknown[]): unknown[];
protected abstract serialize(data: unknown[]): string;
}
class CsvExporter extends DataExporter {
protected filter(data: unknown[]): unknown[] {
// Фильтруем пустые строки
return (data as string[]).filter(Boolean);
}
protected transform(data: unknown[]): unknown[] {
return (data as string[]).map(s => s.trim());
}
protected serialize(data: unknown[]): string {
return (data as string[]).join(',');
}
}
const exporter = new CsvExporter();
console.log(exporter.export(['Alice', '', ' Bob', 'Carol']));
// Alice,Bob,Carol
Пример: Abstract class vs Interface
// Интерфейс — только контракт, без реализации, без состояния
interface Logger {
log(message: string): void;
}
// Абстрактный класс — контракт + общая реализация + состояние
abstract class BaseLogger implements Logger {
private readonly prefix: string;
constructor(prefix: string) {
this.prefix = prefix;
}
// Конкретный метод использует абстрактный
log(message: string): void {
this.write(`[${this.prefix}] ${message}`);
}
// Каждый наследник решает, куда писать
protected abstract write(line: string): void;
}
class ConsoleLogger extends BaseLogger {
protected write(line: string): void {
console.log(line);
}
}
class FileLogger extends BaseLogger {
protected write(line: string): void {
// Запись в файл
process.stdout.write(line + '\n');
}
}
const logger: Logger = new ConsoleLogger('APP');
logger.log('Старт'); // [APP] Старт
Типичные ошибки
Путают abstract class с interface, не понимая, что abstract class компилируется в JS-код и может хранить состояние
Думают, что в abstract class все методы должны быть абстрактными, забывая о возможности конкретных реализаций
Не знают, что попытка создать экземпляр abstract class — это ошибка на этапе компиляции TypeScript, а не рантайма
Забывают, что наследник должен реализовать все abstract-методы, иначе TypeScript выдаст ошибку
Используют abstract class там, где достаточно interface, добавляя лишнюю иерархию наследования


