abstract классы

Abstract классы в TypeScript — классы, помеченные ключевым словом abstract, которые нельзя инстанциировать напрямую; они могут содержать как реализованные методы (шаблон), так и абстрактные члены (abstract), которые обязаны реализовать наследники.

Зачем нужно

Abstract классы задают общий контракт и частичную реализацию для группы классов, когда нужно что-то среднее между интерфейсом (только контракт) и обычным классом (полная реализация). Они идеальны для паттернов Template Method, когда алгоритм общий, а детали — разные.

Где используется

  • Паттерн Template Method — общий алгоритм, разные шаги
  • Базовые классы для семейства экспортёров, парсеров, обработчиков
  • Слой Repository с общей логикой (пагинация, кэш) и абстрактными методами хранилища
  • Абстрактные компоненты UI-фреймворков

Основной контент

Базовый синтаксис

abstract class Animal {
  // Обычный метод — есть реализация
  move: void {
    console.log(`${this.name} is moving`);
  }

  // Абстрактный метод — нет реализации, наследник ОБЯЗАН реализовать
  abstract makeSound: string;

  // Абстрактное свойство
  abstract readonly name: string;
}

// new Animal(); // Error: Cannot create an instance of an abstract class

class Cat extends Animal {
  readonly name = "Cat";
  makeSound: string {
    return "Meow";
  }
}

class Dog extends Animal {
  readonly name = "Dog";
  makeSound: string {
    return "Woof";
  }
}

const cat = new Cat();
cat.move;      // "Cat is moving" — из Animal
cat.makeSound; // "Meow" — реализация Cat

Паттерн Template Method

abstract class DataExporter {
  // Шаблонный метод — определяет алгоритм
  async export(data: unknown): Promise<void> {
    const validated = await this.validate(data);
    const transformed = this.transform(validated);
    await this.save(transformed);
    this.logSuccess(data.length);
  }

  // Шаги — абстрактные, разные для каждого наследника
  protected abstract validate(data: unknown): Promise<unknown>;
  protected abstract transform(data: unknown): string;
  protected abstract save(content: string): Promise<void>;

  // Общий шаг с реализацией
  private logSuccess(count: number): void {
    console.log(`Exported ${count} items`);
  }
}

class CsvExporter extends DataExporter {
  protected async validate(data: unknown) { return data; }
  protected transform(data: unknown) {
    return data.map((row) => JSON.stringify(row)).join("\n");
  }
  protected async save(content: string) {
    await fs.writeFile("output.csv", content);
  }
}

class JsonExporter extends DataExporter {
  protected async validate(data: unknown) { return data; }
  protected transform(data: unknown) { return JSON.stringify(data, null, 2); }
  protected async save(content: string) {
    await fs.writeFile("output.json()", content);
  }
}

Abstract class vs interface

// Interface — только контракт, без реализации
interface Flyable {
  fly: void;
}

// Abstract class — контракт + частичная реализация
abstract class Vehicle {
  protected speed = 0;

  accelerate(delta: number): void {
    this.speed += delta; // общая реализация
  }

  abstract brake: void; // каждый реализует по-своему
}

// Ключевое различие: abstract class может иметь состояние и конструктор
abstract class Base {
  constructor(protected readonly id: string) {}
  abstract describe: string;
}

abstract конструктор

// Тип «конструктор abstract класса» — для фабрик
type Constructor<T> = new (...args: any) => T;
type AbstractConstructor<T> = abstract new (...args: any) => T;

function createInstance<T>(Cls: Constructor<T>): T {
  return new Cls();
}

Частые ошибки

  • Инстанциировать abstract классnew AbstractClass — ошибка компиляции.
  • Не реализовать все abstract члены — компилятор потребует реализацию каждого abstract метода/свойства в конкретном наследнике.
  • Использовать abstract там где достаточно interface — если нет общей реализации, интерфейс проще и не создаёт рантайм-артефактов.
  • Не вызывать super в наследнике — если abstract класс имеет конструктор, наследник обязан вызвать super(...) первым.

Связанные темы

Ресурсы