<h2>Entendendo Herança em TypeScript</h2>
<p>A herança é um dos pilares da Programação Orientada a Objetos e permite que uma classe filha reutilize propriedades e métodos de uma classe pai. Em TypeScript, implementamos herança através da palavra-chave <code>extends</code>. Esse mecanismo não é apenas uma forma de reutilizar código — é uma forma de estabelecer relacionamentos semânticos entre tipos, criando uma hierarquia lógica que representa melhor o domínio do seu problema.</p>
<p>Quando você cria uma classe filha que herda de uma classe pai, ela obtém acesso a todos os membros públicos e protegidos da classe pai. O construtor da classe filha deve chamar o construtor da classe pai usando <code>super()</code>, garantindo que a inicialização ocorra corretamente. Veja um exemplo prático:</p>
<pre><code class="language-typescript">class Animal {
protected nome: string;
protected idade: number;
constructor(nome: string, idade: number) {
this.nome = nome;
this.idade = idade;
}
descrever(): string {
return ${this.nome} tem ${this.idade} anos;
}
fazer_som(): void {
console.log("Som genérico de animal");
}
}
class Cachorro extends Animal {
private raca: string;
constructor(nome: string, idade: number, raca: string) {
super(nome, idade);
this.raca = raca;
}
descrever(): string {
return ${super.descrever()}, é um ${this.raca};
}
fazer_som(): void {
console.log("Au au!");
}
buscar(): void {
console.log(${this.nome} está buscando a bolinha);
}
}
const dog = new Cachorro("Rex", 5, "Labrador");
console.log(dog.descrever()); // Rex tem 5 anos, é um Labrador
dog.fazer_som(); // Au au!
dog.buscar(); // Rex está buscando a bolinha</code></pre>
<p>Note que a classe <code>Cachorro</code> redefiniu o método <code>fazer_som()</code> e <code>descrever()</code>. Isso demonstra que a herança não é apenas cópia de código — ela permite que subclasses especializem o comportamento herdado de forma segura.</p>
<h3>Modificadores de Acesso na Herança</h3>
<p>Em TypeScript, temos três modificadores que controlam a visibilidade de membros: <code>public</code>, <code>protected</code> e <code>private</code>. O modificador <code>protected</code> é especialmente importante na herança, pois permite que membros sejam acessados pela classe filha, mas não pelo mundo externo.</p>
<pre><code class="language-typescript">class Veiculo {
public marca: string;
protected velocidade_maxima: number;
private numero_serie: string;
constructor(marca: string, velocidade_maxima: number, numero_serie: string) {
this.marca = marca;
this.velocidade_maxima = velocidade_maxima;
this.numero_serie = numero_serie;
}
obter_info(): string {
return Marca: ${this.marca};
}
}
class Carro extends Veiculo {
private portas: number;
constructor(marca: string, velocidade_maxima: number, numero_serie: string, portas: number) {
super(marca, velocidade_maxima, numero_serie);
this.portas = portas;
}
acelerar(): void {
console.log(Carro acelerando até ${this.velocidade_maxima} km/h);
}
// Erro: this.numero_serie não é acessível (private)
// exibir_serie(): string { return this.numero_serie; }
}
const meuCarro = new Carro("Toyota", 200, "ABC123", 4);
console.log(meuCarro.marca); // OK - public
meuCarro.acelerar(); // OK - usa protected
// console.log(meuCarro.numero_serie); // Erro em tempo de compilação</code></pre>
<h2>Polimorfismo: Múltiplas Formas de um Mesmo Contrato</h2>
<p>Polimorfismo significa "muitas formas". Em TypeScript, ele se manifesta quando diferentes classes filhas implementam o mesmo método herdado de formas distintas. Isso permite que você trate objetos de diferentes tipos através de uma referência de tipo da classe pai, mantendo o comportamento correto de cada um.</p>
<p>O polimorfismo real só funciona quando há uma relação de herança ou quando há um contrato comum — geralmente representado por uma interface. Sem isso, você apenas tem métodos com o mesmo nome em classes diferentes, o que não é verdadeiro polimorfismo.</p>
<pre><code class="language-typescript">abstract class Funcionario {
protected nome: string;
protected salario: number;
constructor(nome: string, salario: number) {
this.nome = nome;
this.salario = salario;
}
abstract calcular_bonus(): number;
exibir_detalhes(): void {
console.log(Funcionário: ${this.nome});
}
}
class Desenvolvedor extends Funcionario {
calcular_bonus(): number {
return this.salario * 0.2;
}
}
class Gerente extends Funcionario {
calcular_bonus(): number {
return this.salario * 0.3;
}
}
class Estagiario extends Funcionario {
calcular_bonus(): number {
return this.salario * 0.05;
}
}
function processar_salario(funcionario: Funcionario): void {
funcionario.exibir_detalhes();
const bonus = funcionario.calcular_bonus();
console.log(Bônus: R$ ${bonus.toFixed(2)}\n);
}
const equipe: Funcionario[] = [
new Desenvolvedor("Ana", 5000),
new Gerente("Carlos", 8000),
new Estagiario("Bruno", 2000)
];
equipe.forEach(processar_salario);</code></pre>
<p>Aqui, a função <code>processar_salario()</code> recebe qualquer tipo que seja <code>Funcionario</code>. Em tempo de execução, o TypeScript chama o método <code>calcular_bonus()</code> correto de cada objeto. Isso é polimorfismo em ação — a mesma interface, comportamentos diferentes.</p>
<h3>Classes Abstratas vs. Interfaces</h3>
<p>Classes abstratas são úteis quando você quer compartilhar código e estado entre subclasses. Interfaces, por sua vez, definem apenas contratos — não contêm implementação ou estado. Escolha entre elas baseado em suas necessidades: use classes abstratas quando há código comum e estado compartilhado; use interfaces quando quer apenas definir um contrato que múltiplas classes podem implementar.</p>
<pre><code class="language-typescript">// Interface define contrato
interface Pagavel {
processar_pagamento(valor: number): boolean;
obter_saldo(): number;
}
// Classe abstrata define comportamento parcial e estado
abstract class ContaBancaria implements Pagavel {
protected saldo: number;
protected titular: string;
constructor(titular: string, saldo_inicial: number) {
this.titular = titular;
this.saldo = saldo_inicial;
}
obter_saldo(): number {
return this.saldo;
}
abstract processar_pagamento(valor: number): boolean;
exibir_extrato(): void {
console.log(${this.titular}: R$ ${this.saldo.toFixed(2)});
}
}
class ContaCorrente extends ContaBancaria {
private limite: number;
constructor(titular: string, saldo_inicial: number, limite: number) {
super(titular, saldo_inicial);
this.limite = limite;
}
processar_pagamento(valor: number): boolean {
if (valor <= this.saldo + this.limite) {
this.saldo -= valor;
return true;
}
return false;
}
}
class ContaPoupanca extends ContaBancaria {
processar_pagamento(valor: number): boolean {
if (valor <= this.saldo) {
this.saldo -= valor;
return true;
}
return false;
}
}
const contas: Pagavel[] = [
new ContaCorrente("João", 1000, 500),
new ContaPoupanca("Maria", 2000, 0)
];
contas.forEach(conta => {
const pagou = conta.processar_pagamento(300);
console.log(Pagamento processado: ${pagou});
console.log(Novo saldo: R$ ${conta.obter_saldo().toFixed(2)}\n);
});</code></pre>
<h2>Interfaces: Contratos Sem Implementação</h2>
<p>Uma interface em TypeScript é um contrato que define quais propriedades e métodos um objeto deve ter. Ao contrário de classes abstratas, interfaces não contêm nenhuma implementação — apenas assinaturas de métodos e tipos de propriedades. Isso as torna ideais para definir contratos que múltiplas classes não relacionadas podem cumprir.</p>
<p>A verdadeira força das interfaces aparece quando você usa herança de interface. Uma classe pode implementar múltiplas interfaces, criando uma flexibilidade que herança simples não ofereceria. Uma interface também pode estender outras interfaces, formando hierarquias de contratos.</p>
<pre><code class="language-typescript">interface Notificavel {
enviar_notificacao(mensagem: string): void;
}
interface Persistivel {
salvar(): void;
carregar(id: string): void;
}
interface Usuario extends Notificavel, Persistivel {
id: string;
nome: string;
email: string;
}
class UsuarioWeb implements Usuario {
id: string;
nome: string;
email: string;
constructor(id: string, nome: string, email: string) {
this.id = id;
this.nome = nome;
this.email = email;
}
enviar_notificacao(mensagem: string): void {
console.log([Email para ${this.email}] ${mensagem});
}
salvar(): void {
console.log(Salvando usuário ${this.id} no banco de dados...);
}
carregar(id: string): void {
console.log(Carregando usuário com ID: ${id});
}
}
class UsuarioMobile implements Usuario {
id: string;
nome: string;
email: string;
constructor(id: string, nome: string, email: string) {
this.id = id;
this.nome = nome;
this.email = email;
}
enviar_notificacao(mensagem: string): void {
console.log([Push notification] ${mensagem});
}
salvar(): void {
console.log(Salvando usuário ${this.id} em cache local...);
}
carregar(id: string): void {
console.log(Carregando usuário com ID: ${id} do cache);
}
}
const usuarios: Usuario[] = [
new UsuarioWeb("1", "Alice", "alice@email.com"),
new UsuarioMobile("2", "Bob", "bob@email.com")
];
usuarios.forEach(usuario => {
usuario.enviar_notificacao("Bem-vindo!");
usuario.salvar();
});</code></pre>
<h3>Tipos Genéricos com Interfaces</h3>
<p>As interfaces ganham ainda mais poder quando combinadas com genéricos. Isso permite criar contratos reutilizáveis que funcionam com qualquer tipo de dado, mantendo a segurança de tipos.</p>
<pre><code class="language-typescript">interface Repositorio<T> {
criar(item: T): void;
obter(id: string): T | null;
listar(): T[];
deletar(id: string): boolean;
}
interface Produto {
id: string;
nome: string;
preco: number;
}
class RepositorioProduto implements Repositorio<Produto> {
private produtos: Map<string, Produto> = new Map();
criar(item: Produto): void {
this.produtos.set(item.id, item);
console.log(Produto "${item.nome}" criado);
}
obter(id: string): Produto | null { return this.produtos.get(id) || null;
}
listar(): Produto[] {
return Array.from(this.produtos.values());
}
deletar(id: string): boolean {
return this.produtos.delete(id);
}
}
const repo = new RepositorioProduto();
repo.criar({ id: "1", nome: "Notebook", preco: 3000 });
repo.criar({ id: "2", nome: "Mouse", preco: 50 });
console.log("Produtos cadastrados:");
repo.listar().forEach(p => {
console.log(- ${p.nome}: R$ ${p.preco});
});
const produto = repo.obter("1");
console.log(\nProduto obtido: ${produto?.nome});</code></pre>
<h2>Casos Práticos: Quando Usar Cada Conceito</h2>
<p>A decisão entre herança, classes abstratas e interfaces não é trivial. Herança é ideal quando há uma relação "é um(a)" clara — um <code>Cachorro</code> <strong>é um</strong> <code>Animal</code>. Classes abstratas são perfeitas quando você quer forçar um contrato e compartilhar código comum entre subclasses. Interfaces funcionam melhor quando você precisa descrever múltiplos comportamentos independentes que diferentes classes podem ter.</p>
<p>Um padrão comum em projetos reais é usar interfaces para definir contratos e classes abstratas para fornecer implementação parcial. Isso combina o melhor dos dois mundos: a flexibilidade das interfaces com o suporte a código compartilhado.</p>
<pre><code class="language-typescript">// Definir interface para o contrato
interface TransportadorDados {
enviar(dados: string): Promise<boolean>;
receber(): Promise<string | null>;
}
// Classe abstrata com comportamento comum
abstract class TransportadorBase implements TransportadorDados {
protected log: string[] = [];
registrar_log(mensagem: string): void {
this.log.push([${new Date().toISOString()}] ${mensagem});
}
obter_log(): string[] {
return this.log;
}
abstract enviar(dados: string): Promise<boolean>;
abstract receber(): Promise<string | null>;
}
// Implementações concretas
class TransportadorHTTP extends TransportadorBase {
async enviar(dados: string): Promise<boolean> {
this.registrar_log(Enviando via HTTP: ${dados});
// Simular envio
return true;
}
async receber(): Promise<string | null> {
this.registrar_log("Recebendo via HTTP");
return "dados recebidos";
}
}
class TransportadorWebSocket extends TransportadorBase {
async enviar(dados: string): Promise<boolean> {
this.registrar_log(Enviando via WebSocket: ${dados});
return true;
}
async receber(): Promise<string | null> {
this.registrar_log("Recebendo via WebSocket");
return "dados em tempo real";
}
}
async function comunicar(transportador: TransportadorDados): Promise<void> {
await transportador.enviar("Olá servidor");
const resposta = await transportador.receber();
console.log(Resposta: ${resposta});
}
// Usar com qualquer implementação
comunicar(new TransportadorHTTP());
comunicar(new TransportadorWebSocket());</code></pre>
<h2>Conclusão</h2>
<p>Herança e polimorfismo são mecanismos fundamentais que, quando bem compreendidos, transformam como você estrutura código orientado a objetos. <strong>Primeiro ponto</strong>: herança estabelece hierarquias semânticas e promove reuso de código, mas deve ser usada quando há uma relação "é um(a)" genuína — não use herança apenas para compartilhar código. <strong>Segundo ponto</strong>: polimorfismo através de herança e interfaces permite que você escreva código genérico que funciona com múltiplos tipos, reduzindo duplicação e aumentando a manutenibilidade. <strong>Terceiro ponto</strong>: interfaces são mais flexíveis que herança para definir contratos, permitindo que uma classe implemente múltiplas interfaces, enquanto classes abstratas são melhores para compartilhar implementação comum.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://www.typescriptlang.org/docs/handbook/2/classes.html" target="_blank" rel="noopener noreferrer">TypeScript Handbook - Classes</a></li>
<li><a href="https://www.typescriptlang.org/docs/handbook/2/objects.html" target="_blank" rel="noopener noreferrer">TypeScript Handbook - Interfaces</a></li>
<li><a href="https://developer.mozilla.org/pt-BR/docs/Web/JavaScript/Inheritance_and_the_prototype_chain" target="_blank" rel="noopener noreferrer">MDN - Herança e Cadeia de Protótipos</a></li>
<li><a href="https://basarat.gitbook.io/typescript/oop" target="_blank" rel="noopener noreferrer">TypeScript Deep Dive - OOP</a></li>
<li><a href="https://refactoring.guru/design-patterns" target="_blank" rel="noopener noreferrer">Refactoring.guru - Design Patterns</a></li>
</ul>
<p><!-- FIM --></p>