Uma classe TypeScript herda de outra com extends. A subclasse recebe todos os campos e métodos do pai, chama o construtor do pai com super(...) e pode sobrescrever métodos, e o TypeScript verifica que cada sobrescrita continua compatível com os tipos do pai.
Isso é herança de classes do JavaScript (veja herança em JavaScript para a cadeia de protótipos por trás dela). O TypeScript acrescenta as verificações de tipo: um Dog pode ser atribuído em qualquer lugar em que um Animal é esperado, e o compilador verifica a sobrescrita e a chamada a super.
Sobrescrevendo métodos
Um método da subclasse com o mesmo nome substitui o do pai. O tipo dele precisa poder ser atribuído ao do pai: parâmetros iguais ou compatíveis, tipo de retorno igual ou mais estreito. Senão, qualquer código que guarda um Animal poderia chamar o método com argumentos que a subclasse não consegue tratar.
Trocar o parâmetro por value: string geraria o erro TS2416: Property 'format' in type 'CurrencyFormatter' is not assignable to the same property in base type 'Formatter'. O loop mostra o porquê: ele chama format com um número em cada elemento, sem saber qual subclasse tem.
Há uma brecha. Os parâmetros de métodos são comparados de forma bivariante, então uma sobrescrita que aceita um tipo mais estreito, como format(value: 1 | 2), compila, mesmo que o loop ainda possa passar 1234567 para ela. Os tipos de retorno são verificados de forma estrita: retornar number em vez de string gera TS2416.
A palavra-chave override e o noImplicitOverride
O override diz "este membro substitui um da classe base", e o compilador cobra isso. Se a base não tem esse membro (um erro de digitação, ou um método que alguém renomeou no pai), é um erro:
index.ts(9,14): error TS4117: This member cannot have an 'override' modifier because it is not declared in the base class 'Animal'. Did you mean 'speak'?
Sem o override, esse erro de digitação compila e speak() retorna "..." em silêncio. A palavra-chave só ajuda onde é escrita, então ligue o noImplicitOverride no tsconfig.json. Assim, todo método que sobrescreve um membro da base precisa dizer isso, e um speak() simples em uma subclasse gera o erro TS4114 (This member must have an 'override' modifier because it overrides a member in the base class 'Animal').
{
"compilerOptions": {
"strict": true,
"noImplicitOverride": true
}
}
O noImplicitOverride não faz parte do strict; você precisa adicioná-lo. O override é apagado do JavaScript compilado.
Membros protected em subclasses
Membros private do pai são invisíveis na subclasse. Membros protected são visíveis, e esse é o motivo de o protected existir: ele é a API que uma classe pai oferece às filhas.
Veja modificadores de acesso para as regras completas, incluindo por que nem private nem protected são aplicados em tempo de execução.
Redeclarando um campo com um tipo mais estreito
Uma subclasse muitas vezes sabe que um campo guarda algo mais específico do que o pai diz. Redeclará-lo como pet: Dog tem um efeito em tempo de execução com os class fields modernos: o campo da subclasse é inicializado com undefined depois que super() retorna, apagando o que o construtor do pai guardou. A menos que o construtor da subclasse o atribua de novo, o TypeScript aponta isso como TS2612 (Property 'pet' will overwrite the base property in 'Home'). Use declare para mudar só o tipo:
O declare não gera nada, então o valor definido pelo construtor de Home sobrevive. O construtor que só aceita um Dog é o que torna verdadeiro o tipo mais estreito.
implements vs extends
As duas palavras-chave respondem a perguntas diferentes, e uma classe pode usar as duas.
extends | implements | |
|---|---|---|
| Funciona com | uma classe | qualquer número de interfaces (ou type aliases de tipos de objeto) |
| Traz código | sim: campos, métodos, construtor | não, só verifica o formato |
| Existe em tempo de execução | sim, a cadeia de protótipos | não, é apagado |
instanceof funciona | sim | não |
| Uso típico | compartilhar comportamento real | prometer um formato ao resto do código |
Uma classe só pode estender uma classe (TS1174: Classes can only extend a single class). Para combinar comportamento de vários lugares, implemente várias interfaces e compartilhe código por composição (um campo que guarda um objeto auxiliar) ou por mixins.
Mixins
Um mixin é uma função que recebe uma classe e retorna uma subclasse dela. Ele dá uma forma de herança múltipla que passa na verificação de tipos:
O any[] em Constructor é obrigatório: o TypeScript só aceita como base de mixin uma classe cujo construtor recebe um único rest parameter do tipo any[]. Mixins funcionam, mas deixam os tipos e os stack traces mais difíceis de ler, então tente composição primeiro.
Erros comuns
- Chamar um método sobrescrevível a partir do construtor da base. O construtor da base roda antes de os campos da subclasse serem inicializados, então uma sobrescrita que lê
this.itemsvêundefined, e uma chamada comothis.items.push(x)lança umTypeErrorem tempo de execução. O TypeScript não pega isso. - Esquecer o
super(...)ou chamá-lo tarde. ErrosTS2377eTS17009; o pai precisa montar o objeto antes que a subclasse use othis. - Redeclarar um campo do pai sem
declare. O campo da subclasse o reinicia paraundefined. - Hierarquias profundas. Cada nível acopla a filha aos detalhes internos do pai. Dois níveis costumam bastar; além disso, prefira composição ou interfaces.
Perguntas frequentes
Como funciona a herança no TypeScript?
Uma classe herda de outra com extends: class Dog extends Animal. A subclasse recebe os campos e métodos do pai, chama o construtor do pai com super(...) e pode sobrescrever métodos. O TypeScript verifica que as sobrescritas continuam compatíveis com os tipos do pai.
O que a palavra-chave override faz no TypeScript?
O override marca um método ou propriedade que substitui um da classe base. Se a classe base não tem nenhum membro com esse nome, o compilador aponta o TS4117, o que pega erros de digitação e membros renomeados no pai. Com noImplicitOverride ligado, toda sobrescrita precisa ter a palavra-chave (senão, TS4114).
Como chamar um método da classe pai no TypeScript?
Use super.methodName(...) dentro do método da subclasse. No construtor, super(...) chama o construtor do pai e precisa rodar antes de qualquer uso do this.
O TypeScript suporta herança múltipla?
Não. Uma classe só pode estender uma classe (TS1174: Classes can only extend a single class). Ela pode implementar qualquer número de interfaces, e mixins (funções que recebem uma classe e retornam uma subclasse) combinam comportamento de várias fontes.
Qual é a diferença entre extends e implements?
extends herda código de verdade de uma classe pai. implements só verifica se uma classe corresponde ao formato de uma interface e não acrescenta nada em tempo de execução. Uma classe pode fazer os dois: class Dog extends Animal implements Pet.