Classes abstratas no TypeScript permitem definir uma estrutura compartilhada para um grupo de classes relacionadas sem se comprometer com uma implementação completa de antemão. São um daqueles recursos que parecem acadêmicos até você estar mantendo uma base de código com seis classes ligeiramente diferentes mas que deveriam ser consistentes, e perceber que precisava delas há três meses.

Entendendo Classes Abstratas

Uma classe abstrata não pode ser instanciada diretamente. Ela é um blueprint que outras classes herdam. Pode conter métodos totalmente implementados ao lado de métodos abstratos, que são declarações sem corpo que as subclasses devem implementar.

O TypeScript usa a palavra-chave abstract para marcar tanto a classe quanto os métodos que exigem implementação nas subclasses.

Classes Abstratas no TypeScript

Veja um exemplo simples:

abstract class Animal {
  abstract makeSound(): void; // Abstract method

  move(): void {
    console.log("Moving along!");
  }
}

Animal é abstrata: declara makeSound() sem implementá-lo e fornece um método concreto move(). Qualquer classe que estenda Animal deve fornecer sua própria implementação de makeSound().

Veja como você pode estender a classe Animal:

class Dog extends Animal {
  makeSound(): void {
    console.log("Bark!");
  }
}

const myDog = new Dog();
myDog.makeSound(); // Outputs: Bark!
myDog.move(); // Outputs: Moving along!

Dog herda move() gratuitamente e satisfaz o contrato de makeSound(). Tente instanciar Animal diretamente e o compilador TypeScript rejeita isso em tempo de build, antes que qualquer bug chegue à produção.

Quando Usar Classes Abstratas

Classes abstratas funcionam melhor quando um grupo de classes compartilha implementação real, não apenas um contrato. Um exemplo comum são veículos com sequências de inicialização diferentes:

abstract class Vehicle {
  abstract startEngine(): void;

  stopEngine(): void {
    console.log("Engine stopped.");
  }
}

Car e Motorcycle implementariam startEngine() de formas diferentes, mas ambos herdam o mesmo stopEngine() sem duplicação.

Se as classes não compartilham nenhuma implementação, opte por uma interface.

Vantagens de Usar Classes Abstratas

  • A lógica compartilhada fica em um único lugar. Altere stopEngine() uma vez e todas as subclasses recebem a atualização.
  • Polimorfismo: código que referencia o tipo abstrato funciona com qualquer subclasse concreta.
  • O compilador impõe o contrato. Implementações ausentes são erros de compilação, não surpresas em tempo de execução.

Classes Abstratas vs. Interfaces

Ambas definem contratos, mas a distinção importa na prática.

Classes abstratas podem conter métodos implementados e estado de instância. Interfaces são puramente estruturais: declaram como um tipo deve parecer, sem nenhuma implementação.

Escolha uma classe abstrata quando tiver código compartilhado a distribuir. Escolha uma interface quando estiver descrevendo uma capacidade que classes não relacionadas possam adotar de forma independente.

Exemplo Real: Implementando uma Hierarquia de Formas

Considere uma aplicação que trabalha com formas geométricas. Você pode definir uma classe abstrata Shape:

abstract class Shape {
  abstract area(): number;

  toString(): string {
    return `Shape with area ${this.area()}`;
  }
}

Subclasses como Circle e Rectangle implementariam o método area():

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }

  area(): number {
    return Math.PI * this.radius ** 2;
  }
}

class Rectangle extends Shape {
  constructor(private width: number, private height: number) {
    super();
  }

  area(): number {
    return this.width * this.height;
  }
}

const circle = new Circle(5);
console.log(circle.toString()); // Outputs: Shape with area 78.53981633974483

const rectangle = new Rectangle(10, 5);
console.log(rectangle.toString()); // Outputs: Shape with area 50

toString() é escrito uma única vez em Shape e chama o area() que a classe concreta fornece. Adicionar um Triangle depois significa implementar um único método, não copiar boilerplate.

Conclusão

Classes abstratas são a ferramenta certa quando você tem implementação compartilhada a distribuir e um contrato a impor. Elas capturam implementações de métodos ausentes em tempo de compilação e mantêm a lógica comum em um único lugar. Se você só precisa descrever uma forma, use uma interface. Se precisa compartilhar código e impor uma forma, use uma classe abstrata.