TypeScript confirma que métodos possuem nomes e tipos compatíveis. Ele não consegue garantir que uma implementação preserve o significado do contrato.
O Liskov Substitution Principle, ou LSP, trata dessa parte comportamental. Um consumidor deve poder trocar uma implementação por outra sem receber resultados inesperados ou precisar conhecer exceções escondidas.
O problema que o princípio resolve
Considere um armazenamento de arquivos usado por um processo de auditoria. Uma implementação grava em disco. Outra foi criada para um ambiente somente leitura, mas mantém a mesma interface lançando erro ao salvar.
O compilador aceita as duas. O processo, porém, não pode substituí-las com segurança.
Substituição é comportamento
Uma implementação respeita LSP quando mantém as expectativas observáveis do contrato. Isso inclui retorno, efeitos, erros e estado.
Na prática, um subtipo não deve exigir pré-condições mais fortes, entregar garantias mais fracas nem introduzir efeitos incompatíveis. Se o chamador precisa perguntar qual implementação recebeu, a abstração perdeu valor.
Exemplo ruim
interface AuditStorage {
save(fileName: string, content: string): Promise<void>;
read(fileName: string): Promise<string>;
}
class ReadOnlyAuditStorage implements AuditStorage {
constructor(private readonly files: Map<string, string>) {}
async save(): Promise<void> {
throw new Error("Este armazenamento não permite escrita");
}
async read(fileName: string): Promise<string> {
const content = this.files.get(fileName);
if (!content) {
throw new Error("Arquivo não encontrado");
}
return content;
}
}
class GenerateAuditReport {
constructor(private readonly storage: AuditStorage) {}
async execute(reportId: string, content: string): Promise<void> {
await this.storage.save(`${reportId}.log`, content);
}
}Por que o exemplo é ruim?
GenerateAuditReport recebe um contrato que promete save. A implementação somente leitura aceita a chamada pelo tipo, mas a rejeita por definição.
Não é uma falha transitória, como disco indisponível. É uma incapacidade permanente. O contrato afirma uma capacidade que o objeto não possui.
Adicionar instanceof no caso de uso apenas revelaria a quebra e acoplaria o consumidor aos detalhes concretos.
Refatoração
As capacidades podem ser expressas separadamente:
interface AuditReader {
read(fileName: string): Promise<string>;
}
interface AuditWriter {
save(fileName: string, content: string): Promise<void>;
}
class MemoryAuditArchive implements AuditReader, AuditWriter {
private readonly files = new Map<string, string>();
async save(fileName: string, content: string): Promise<void> {
this.files.set(fileName, content);
}
async read(fileName: string): Promise<string> {
const content = this.files.get(fileName);
if (content === undefined) {
throw new Error(`Auditoria não encontrada: ${fileName}`);
}
return content;
}
}
class ReadOnlyAuditArchive implements AuditReader {
constructor(private readonly files: ReadonlyMap<string, string>) {}
async read(fileName: string): Promise<string> {
const content = this.files.get(fileName);
if (content === undefined) {
throw new Error(`Auditoria não encontrada: ${fileName}`);
}
return content;
}
}Exemplo bom
Cada consumidor pede somente a garantia que utiliza:
class GenerateAuditReport {
constructor(private readonly writer: AuditWriter) {}
async execute(reportId: string, content: string): Promise<string> {
const fileName = `${reportId}.log`;
await this.writer.save(fileName, content);
return fileName;
}
}
class ViewAuditReport {
constructor(private readonly reader: AuditReader) {}
execute(fileName: string): Promise<string> {
return this.reader.read(fileName);
}
}Qualquer AuditWriter deve persistir o conteúdo ou comunicar uma falha operacional prevista. Uma implementação incapaz de escrever simplesmente não declara esse contrato.
Contratos precisam dizer mais que os tipos
Duas implementações podem devolver Promise<string> e ainda discordar. Uma pode retornar conteúdo vazio para arquivo ausente enquanto outra lança erro. Se isso afeta o consumidor, o contrato precisa estabelecer uma regra única.
Nomes, documentação, tipos de resultado e testes de contrato tornam a expectativa visível. Um resultado explícito pode ser útil:
type ReadResult =
| { found: true; content: string }
| { found: false };
interface SafeAuditReader {
read(fileName: string): Promise<ReadResult>;
}Agora ausência não depende de uma convenção escondida.
Testes de contrato
Uma mesma suíte pode ser executada contra implementações diferentes. Ela verifica garantias comuns, como salvar e ler o mesmo conteúdo, sobrescrever de forma previsível e representar ausência da mesma maneira.
Esses testes não substituem testes específicos de infraestrutura. Eles protegem exatamente a propriedade que LSP exige: o consumidor observa o mesmo contrato.
Benefícios
- consumidores não verificam tipos concretos;
- testes usam substitutos confiáveis;
- erros e garantias ficam previsíveis;
- novas implementações entram sem alterar casos de uso;
- contratos representam capacidades reais.
Quando aplicar?
Sempre pergunte: "Posso substituir a classe base por uma classe derivada sem alterar o comportamento esperado pelo consumidor?" Se a resposta for "não", é hora de aplicar o LSP.
Fechamento
Liskov Substitution Principle exige honestidade nas abstrações. Compartilhar uma assinatura não basta; as implementações precisam preservar as garantias que permitem ao consumidor trabalhar sem surpresas.
Contratos menores, semântica explícita e testes compartilhados transformam substituição de uma promessa teórica em uma propriedade verificável.
Série SOLID
Este artigo faz parte de uma série de artigos: