O que são Classes Abstratas em TypeScript?
Uma classe abstrata é um modelo ou contrato que define a estrutura e o comportamento que suas classes filhas devem seguir. Diferentemente de uma classe comum, uma classe abstrata não pode ser instanciada diretamente. Seu propósito é servir como uma base para outras classes herdarem seus métodos e propriedades.
Em TypeScript, você declara uma classe abstrata usando a palavra-chave abstract. Isso significa que a classe existe para ser estendida, não para criar objetos dela mesma. É um padrão poderoso para impor consistência em projetos grandes, onde múltiplas classes precisam seguir a mesma interface de implementação.
Métodos Abstratos: Definindo Contratos
O que é um Método Abstrato?
Um método abstrato é um método declarado dentro de uma classe abstrata sem implementação. Ele define apenas a assinatura (nome, parâmetros e tipo de retorno), deixando a implementação real para as classes filhas. Isso garante que toda classe que herdar da abstrata obrigatoriamente implemente esse método.
A vantagem principal é que você força o contrato: qualquer desenvolvedor que use sua classe abstrata saberá exatamente quais métodos precisam ser implementados, evitando esquecimentos ou inconsistências.
Sintaxe Básica
abstract class Animal {
abstract fazerSom(): void;
abstract obterEspecie(): string;
}
Aqui, qualquer classe que herdar de Animal deve implementar fazerSom() e obterEspecie(). Se não implementar, o TypeScript lançará um erro em tempo de compilação.
Exemplo Prático: Um Sistema de Pagamentos
Vou mostrar um caso real que você provavelmente enfrentará na carreira: um sistema que processa diferentes tipos de pagamento (cartão, boleto, transferência bancária).
abstract class ProcessadorPagamento {
private codigoTransacao: string;
constructor() {
this.codigoTransacao = Math.random().toString(36).substring(7);
}
// Método abstrato - deve ser implementado pelas classes filhas
abstract validarDados(dados: object): boolean;
// Outro método abstrato
abstract processar(valor: number): Promise<boolean>;
// Método concreto - é opcional implementar em subclasses
obterCodigoTransacao(): string {
return this.codigoTransacao;
}
// Método concreto que usa lógica compartilhada
registrarLog(mensagem: string): void {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${mensagem}`);
}
}
class PagamentoCartao extends ProcessadorPagamento {
validarDados(dados: { numero: string; cvv: string }): boolean {
return dados.numero.length === 16 && dados.cvv.length === 3;
}
async processar(valor: number): Promise<boolean> {
this.registrarLog(`Processando pagamento de R$ ${valor} por cartão`);
// Simula validação com banco
return new Promise((resolve) => {
setTimeout(() => {
const sucesso = valor > 0 && valor < 100000;
this.registrarLog(
`Cartão: ${sucesso ? 'Aprovado' : 'Recusado'} - TX: ${this.obterCodigoTransacao()}`
);
resolve(sucesso);
}, 1000);
});
}
}
class PagamentoBoleto extends ProcessadorPagamento {
validarDados(dados: { codigoBarras: string }): boolean {
return dados.codigoBarras.length === 47;
}
async processar(valor: number): Promise<boolean> {
this.registrarLog(`Processando boleto de R$ ${valor}`);
// Boletos têm processamento mais lento
return new Promise((resolve) => {
setTimeout(() => {
this.registrarLog(
`Boleto: Registrado para compensação - TX: ${this.obterCodigoTransacao()}`
);
resolve(true);
}, 3000);
});
}
}
class PagamentoTransferencia extends ProcessadorPagamento {
validarDados(dados: { banco: string; conta: string }): boolean {
return dados.banco.length > 0 && dados.conta.length >= 8;
}
async processar(valor: number): Promise<boolean> {
this.registrarLog(`Processando transferência de R$ ${valor}`);
return new Promise((resolve) => {
setTimeout(() => {
this.registrarLog(
`Transferência: Iniciada - TX: ${this.obterCodigoTransacao()}`
);
resolve(true);
}, 500);
});
}
}
// Usando as classes
async function processarCheckout() {
const pagamentoCartao = new PagamentoCartao();
const dadosCartao = { numero: '1234567890123456', cvv: '123' };
if (pagamentoCartao.validarDados(dadosCartao)) {
const aprovado = await pagamentoCartao.processar(150.00);
console.log(`Resultado: ${aprovado ? 'Pagamento realizado' : 'Falha'}\n`);
}
const pagamentoBoleto = new PagamentoBoleto();
const dadosBoleto = { codigoBarras: '12345678901234567890123456789012345678901234567' };
if (pagamentoBoleto.validarDados(dadosBoleto)) {
const processado = await pagamentoBoleto.processar(250.00);
console.log(`Resultado: ${processado ? 'Boleto registrado' : 'Falha'}\n`);
}
}
processarCheckout();
Observe que:
- As três classes implementam obrigatoriamente os métodos abstratos validarDados() e processar()
- Todas herdam o método registrarLog() sem precisar reimplementá-lo
- Você nunca pode fazer new ProcessadorPagamento() — isso vai gerar erro
Quando Usar Classes e Métodos Abstratos
Cenários Ideais
Use classes abstratas quando você tem uma hierarquia de classes relacionadas que compartilham comportamento comum, mas têm implementações diferentes. Os padrões mais comuns são:
- Factory Pattern: Uma classe abstrata define a interface, subclasses criam implementações específicas
- Template Method: A classe abstrata define o "esqueleto" do algoritmo, subclasses implementam detalhes
- Strategy Pattern: Diferentes estratégias implementam a mesma interface abstrata
O que NÃO Fazer
Não use classes abstratas para simples herança de propriedades. Se você apenas quer compartilhar dados entre classes, uma classe regular é suficiente. Classes abstratas devem impor um contrato de implementação, não apenas reutilizar código.
// ❌ ERRADO: Usando abstrato desnecessariamente
abstract class Veiculo {
marca: string;
ano: number;
// Sem nenhum método abstrato definido
}
// ✅ CORRETO: Classe regular é mais apropriada aqui
class Veiculo {
marca: string;
ano: number;
}
Propriedades Abstratas: Um Detalhe Importante
Além de métodos, você também pode definir propriedades abstratas em TypeScript. Isso força as subclasses a declarar essas propriedades:
abstract class Usuario {
abstract id: number;
abstract nome: string;
abstract email: string;
abstract validarEmail(): boolean;
}
class UsuarioAdmin extends Usuario {
id: number;
nome: string;
email: string;
nivelAcesso: number = 10;
constructor(id: number, nome: string, email: string) {
super();
this.id = id;
this.nome = nome;
this.email = email;
}
validarEmail(): boolean {
return this.email.includes('@empresa.com');
}
}
const admin = new UsuarioAdmin(1, 'João', 'joao@empresa.com');
console.log(`Admin validado: ${admin.validarEmail()}`); // true
Propriedades abstratas são úteis quando você quer garantir que cada subclasse tenha certas informações obrigatórias, não apenas comportamentos.
Conclusão
Aprendemos que classes abstratas definem contratos que suas subclasses devem seguir, evitando inconsistências em projetos grandes. Métodos abstratos garantem que comportamentos críticos sejam implementados corretamente em cada variação da classe. A chave é usar abstratas quando você tem múltiplas classes relacionadas que compartilham uma interface comum, mas diferem na implementação — como no exemplo do sistema de pagamentos. Este é um padrão fundamental da programação orientada a objetos e dominar isso coloca você no caminho para escrever código profissional e escalável.