Classes

Classes

Jornada Javascript: Classes

O conteúdo dessa lição se baseia na base dos dados não primitivos que aprendemos na 7° lição da jornada Javascript. Link abaixo!

Para acessar o link da lição, clique aqui.

Como você já deve saber, uma classe é a representação de uma estrutura organizada, feita para facilitar o desenvolvimento, a leitura (por parte dos desenvolvedores) e a manutenção dos nossos códigos.

Sem as classes, cada parte do nosso projeto seria um amontoado de funções e operações agrupadas de difícil legibilidade e manutenção.

Dito isso, acredito que você já tem um conhecimento suficientemente necessário para avançar com classes em Typescript 🙃

Lembrando que essa lição está intimamente ligada com a lição de classes da jornada do desenvolvedor Javascript, ok?

Então se você ainda não terminou aquela lição, já tá mais que na hora de terminar rs

Classes com typescript

Diferente do que você viu no Javascript, graças aos novos recursos do Typescript, aqui nós podemos adicionar novas propriedades a uma determinada classe, tanto é, que aqui você pode iniciar uma classe em conjunto com nossas propriedades.

É como instanciar um objeto com valor de null ou undefined, para que mais tarde, por meio de um método, aquele objeto armazene uma informação importante, que mais tarde será reutilizada pela própria classe.

E assim como qualquer coisa que já vimos até agora no Typescript, essas propriedades e métodos precisam ser tipados!

Dito isso, vamos ver na prática como tudo isso funciona 😉

Criando classes com typescript

Para iniciarmos, vamos criar uma nova classe chamada de Animal:

class Animal{
 //A lógica da classe vem aqui
}

Como estamos vendo no código acima, a sintaxe de uma classe permanece a mesma daquela que vimos na jornada Javascript.

Lembrando que a questão de nomenclatura para nossas classes, geralmente (eu recomendo) que seja seguida a convenção PascalCase, onde cada palavra começa com uma letra maiúscula, onde evitamos o uso de espaços ou sublinhados entre as palavras.

Vamos ver alguns exemplos de nomenclaturas de classes seguindo a convenção PascalCase:

  • UserAccount
  • OrderProcess
  • HttpClient

Com isso em mente, vamos adentrar nas funcionalidades de nossas classes começando pelas propriedades 😄

Definindo propriedades em classes

Supondo que você queira criar três novas propriedades, que serão inseridas dentro da nossa classe Animal, como por exemplo:

  • Nome
  • Reino
  • Peso

Para isso, nós podemos fazer isso da seguinte forma:

class Animal{
 nome: string = "Baleia Comum";
 reino: string = "Animalia";
 peso: number = 48000;
}

Perceba que cada uma de nossas propriedades estão tipadas, assim como qualquer variável que criamos no Typescript.

E que inicialmente elas precisam receber um valor inicial:

  • Nome = Baleia Comum
  • Reino = Animalia
  • Peso = 48000

Caso você queria adicionar uma propriedade que inicialmente não irá armazenar nenhum valor inicial você pode usar o sinal de exclamação (!) para isso, observe:

class Animal{
 nome: string = "Baleia Comum";
 reino: string = "Animalia";
 peso: number = 48000;
 capturado!: boolean;
}

Note que a propriedade sem valor inicial é declarada com o sinal de exclamação (!).

Para usarmos essa classe dentro da nossa aplicação é bem simples, observe:

class Animal{
 nome: string = "Baleia Comum";
 reino: string = "Animalia";
 peso: number = 48000;
 capturado!: boolean;
}

const baleia = new Animal();
console.log(baleia.nome);//Podemos capturar propriedades
baleia.peso = 50000;//Podemos setar valores das propriedades
baleia.capturado = true;//Outro exemplo de setagem de valores

Diferente do Javascript, aqui no Typescript nós não conseguimos adicionar novas propriedades depois que a nossa classe é instanciada (tipo de coisa que é permitida com Javascript):

baleia.filo = "Chordata";//Esse código vai gerar um erro, pois a propriedade filo não existe dentro da classe Animal

Agora que você já sabe cria propriedades e como fazer para acessá-los, vamos entender agora o funcionamento do método construtor 😉

Método Construtor

O método construtor é o primeiro método que é chamado dentro de uma classe logo após a sua instanciação, e que serve para setarmos alguns valores de forma padrão em nossas propriedades.

A sua declaração segue a mesma daquela que conhecemos no Javascript:

class Animal{
 nome!: string;
 peso!: number;
 
 constructor(nome: string, peso: number){
 this.nome = nome;
 this.peso = peso;
 }
}

const baleia = new Animal("Baleia Comum", 48000);

console.log(baleia.nome);
console.log(baleia.peso);

No comando acima, nós criamos o nosso método construtor que recebe por parâmetro os valores que iremos inserir dentro das nossas propriedades.

Lembrando que usamos o termo reservado this, para fazer referência às nossas propriedades que existem dentro da classe (Animal).

Propriedades com ReadOnly

Com o Typescript, nós podemos criar propriedades do tipo "somente leitura" usando o comando readonly. Observe como isso pode ser feito:

class Animal{
 readonly nome: string = "Baleia Comum";
}

const baleia = new Animal();

console.log(baleia.nome);

baleia.nome = "Baleia Orca";//Erro no Typescript, pois a propriedade nome é readonly

Criar propriedades com readonly é essêncial para definirmos uma propriedade que não deve ter seu valor alterado ao longo da execução do programa.

Heranças

Uma classe também pode herdar propriedades e métodos de uma outra classe existente, e isso pode ser feito usando o comando extends para extender as caracteristicas da classe A para uma classe B.

Vamos ver como isso funciona na prática:

class Animal{
 filo!: string;
 peso!: number;
}

class Baleia extends Animal{
 nome!: string;
}

const baleia = new Baleia();
baleia.nome = "Baleia Comum";

//Note que estamos atribuindo valores de propriedades que foram herdadas da classe Animal
baleia.filo = "Chordata";
baleia.peso = 48000;

No código acima, nós temos uma classe chamada de Animal que contém duas propriedades (filo e peso), em seguida, declaramos uma classe chamada Baleia que herdas as propriedades da classe Animal.

Isso significa que a partir de agora, a classe Baleia possui 3 propriedades (nome, filo e peso).

Lembrando que os métodos (que veremos a seguir nesta lição) também são herdados e podem ser chamados de dentro da classe filho.

Método SUPER

Haverá momentos em que algumas classes (que precisam ser herdadas), possuem seus proprios métodos construtores, nas quais nós precisamos acessá-los.

Para exemplificar isso, vamos imaginar que a classe Animal possui um método construtor que seta os valores de duas propriedades:

class Animal{
 filo!: string;
 peso!: number;

 constructor(filo: string, peso: number){
 this.filo = filo;
 this.peso = peso;
 }
}

E que a nossa classe Baleia também tenha um método construtor, que além de receber as variáveis que setam as propriedades herdadas da classe pai, ainda seta as suas próprias propriedades:

class Baleia extends Animal{
 nome!: string;

 constructor(filo: string, peso: number, nome: string){
 this.filo = filo;
 this.peso = peso;
 this.nome = nome;
 }
}

No caso do comando acima, perceba que estamos setando as variáveis filo e peso além de nome, dentro do método construtor.

E se eu te dissesse que você não precisa repetir os comandos this.filo = filo; e this.peso=peso?

Sim, graças ao comando super(filo, peso), a aplicação já entende que nós queremos acionar (e reaproveitar) os comandos this.filo = filo; e this.peso=peso que já foram declarados dentro da classe Animal.  

Observe como isso pode ser feito:

class Baleia extends Animal{
 nome!: string;

 constructor(filo: string, peso: number, nome: string){
 super(filo, peso);
 this.nome = nome;
 }
}

Portanto, o comando super é utilizado para chamar o construtor da classe pai (Animal) a partir da classe filha (Baleia). Fazendo com que ele passe os parâmetros filo e peso para o construtor da classe pai Animal e que os resultados sejam surtidos também na classe filha.

E se a classe pai tivesse qualquer outra chamada de método dentro do construtor? Neste caso, o método também seria acionado em conjunto!

Por fim, basta chamar a classe herdada da forma como vinhamos fazendo antes:

const baleia = new Baleia('Chordata', 48000, 'Baleia Comum');

Métodos

Como você já deve saber, os métodos nada mais são do que as funções que existem dentro de uma classe.

No Typescript, a declaração não costuma ocorrer de forma muito diferente, mas você precisa se atentar na hora em que for montar a sua tipagem, ok?

Vamos ver como você pode declarar métodos com e sem retorno usando o Typescript:

class Animal{
 nome: string = 'Baleia Comum';//Esse é o método de uma classe

 olaMundo(): void{
 console.log('Olá Mundo!');
 }

 retornaNome(): string{
 return this.nome;
 }
}

const baleia = new Animal();

baleia.olaMundo();//Executa uma mensagem no console
console.log(baleia.retornaNome());//Retorna o valor armazenado dentro da propriedade 'nome'

No comando acima, criamos dois métodos (funções) que seguem a sintaxe comum que estamos acostumados no Typescript. O primeiro método que não retorna nada (void), e o segundo que retorna uma string.

Vejamos agora um exemplo de um método que recebe um parâmetro:

class Animal{
 nome: string = 'Baleia Comum';//Esse é o método de uma classe

 olaMundo(nome: string): void{
 console.log('Olá Mundo!' + nome);
 }

 retornaNome(nome: string): string{
 return nome;
 }
}

const baleia = new Animal();

baleia.olaMundo('Micilini');//Executa uma mensagem no console
console.log(baleia.retornaNome('Roll'));//Retorna o valor armazenado dentro da propriedade 'nome'

Nos comandos acima, nos estamos enviando parâmetros do tipo string para dentro de cada um deles.

Comando This

Presente também no Javascript, o this é um comando bastante famoso que serve para nos referirmos a um determinado objeto existente dentro da classe.

Geralmente usamos o this para acessar as propriedades de uma determinada classe (tipo de coisa que vimos anteriormente).

class Animal{
 nome: string = 'Baleia Comum';//Esse é o método de uma classe

 usoDoThis(): void{
 console.log(this.nome);
 }
}

const baleia = new Animal();

baleia.usoDoThis();

No comando acima, criamos um método que faz o uso da palavra reservada this para acessar a propriedade nome e mostrá-la no console.

Caso desejar, você também pode usar o this dentro de um método para atualizar o valor que está armazenado dentro de uma propriedade, observe:

class Animal{
 nome: string = 'Baleia Comum';//Esse é o método de uma classe

 usoDoThis(): void{
 console.log(this.nome);
 }

 usoDoThisDois(nome: string): void{
 this.nome = nome;
 }
}

const baleia = new Animal();

baleia.usoDoThis();
baleia.usoDoThisDois('Cavalo');

Viu como é fácil?

Getters e Setters

Um recurso muito interessante presente na maioria das linguagens que possuem suporte a orientação a objetos, são os métodos getters e setters.

Os métodos getters são usados para retornar propriedades de um determinado objeto, porém, nós podemos modificá-los durante seu retorno.

Já os métodos setters, são usados para atribuir valores às nossas propriedades, porém, nós podemos modificá-los durante o processo.

Vejamos um exemplo disso na prática:

class Animal {
 _peso: number = 48000;

 // Getter para a propriedade peso multiplicado por 2
 get peso(): number {
 return this._peso * 2;
 }

 // Setter para a propriedade peso
 set peso(valor: number) {
 this._peso = valor / 2;
 }
}

// Exemplo de uso
const animal = new Animal();
console.log(animal.peso); // 20 (10 * 2)

animal.peso = 40; // Ajusta o valor interno de _peso para 20 (40 / 2)
console.log(animal.peso); // 40 (20 * 2)

Note que tanto os métodos getters (usamos o get) quanto os setters (usamos o set), nós chamamos esses métodos como se eles fossem uma propriedade, interessante, não?

Além disso, precisei mudar o nome da minha propriedade peso para _peso, pois o Typescript estava entrando em conflito com o nome dos métodos get peso() e set peso().

Lembrando que nem sempre um método get precisa conter o mesmo nome de uma propriedade, observe este outro exemplo abaixo:

class Usuario{
 nome: string = 'Micilini';
 sobrenome: string = 'Roll';

 get nomeCompleto(): string{
 return this.nome + " " + this.sobrenome;
 

}

const micilini = new Usuario();

console.log(micilini.nomeCompleto);

No caso do setter ele deve apenas receber um único parâmetro, sendo assim, o código abaixo não funcionaria como o esperado:

set nomeCompleto(nome: string, sobrenome: string): void{
 this.nome = nome;
 this.sobrenome = sobrenome;
}

Para resolver a situação, você poderia passar o nome completo dentro de um array ou fazer um split de uma string da seguinte forma:

set nomeCompleto(nomeCompleto: string) {
 const [nome, sobrenome] = nomeCompleto.split(' ');
 this.nome = nome;
 this.sobrenome = sobrenome;
}

....

micilini.nomeCompleto = 'João Silva';
console.log(micilini.nomeCompleto); // Saída: "João Silva"

Com isso criamos um método setter que "recebe" mais de um único parâmetro.

Herança com Interfaces

É obvio que a funcionalidade primordial do Typescript (Interfaces) poderia ser reutilizada dentro das nossas classes por meio de herança, ou você achava que não?

Para que uma classe do Typescript herde as características de uma Interface, você precisa aplicar o conceito de herança usando o comando implements em vez de extends.

Vejamos como isso funciona na prática:

interface DNA{
 adenina: number;
 tinina: number;
 citosina: number;
 guanina: number;
 existir(): void;
}

class Animal implements DNA{
 nome: string = "Baleia Comum"
 peso: number = 48000;

 falar(): void{
 console.log('Baleias não falam... ou Falam?');
 }

 //Você precisa implementar as propriedades e métodos de DNA:
 adenina: number = 122
 tinina: number = 988
 citosina: number = 155
 guanina: number = 188

 existir(): void{
 console.log('EU EXISTO!');
 }
}

const baleia = new Animal();

baleia.existir();
baleia.falar();

No comando acima, nós estamos usando o comando implements de modo a herdar as características da interface DNA.

Override

Um override nada mais é do que o fato de você sobrescrever um método herdado de uma classe pai.

Vamos supor que nós temos uma classe chamada Animal que possui o método chamado existir:

class Animal{
 existir():void{
 console.log('Eu sou um ANIMAL e existo!');
 }
}

Em seguida vamos criar uma outra classe Baleia que herda as caractéristicas da classe Animal:

class Baleia extends Animal{
 //Sua lógica aqui...
}

Se você executar o método existir diretamente da classe Baleia, a mensagem que aparecerá no console será: 'Eu sou um ANIMAL e existo!'

const baleia = new Baleia();
baleia.existir();//Eu sou um ANIMAL e existo!

Mas será que é possível redeclarar esse método (existir) na classe Baleia, de modo a mudar sua mensagem? Sim, observe:

class Baleia extends Animal{
 existir(): void{
 console.log('Eu sou uma BALEIA e existo!');
 console.log('Baleias são DEZ!');
 }
}

Para realizar uma sobrescrita (override) basta apenas criar o método com o mesmo nome dentro da classe pai. (Em outras linguagens usaríamos o comando override antes do nomes do método).

Dessa forma, quando chamarmos esse método por meio da classe Baleia, a mensagem será: 'Eu sou uma Baleia e existo!' e 'Baleias são DEZ!'.

const baleia = new Baleia();
baleia.existir();//Eu sou uma Baleia e existo! - Baleias são DEZ!

Visibilidade de Métodos e Propriedades

Assim como toda linguagem de programação que possui suporte a orientação a objetos, a grande maioria delas possui suporte a visibilidade.

Que é um conceito na qual nós podemos expor, reservar ou esconder nossos métodos e propriedades da nossa classe, mediante a outras classes e acessos externos.

Os modificadores de visibilidade que podemos usar em nossas classes são apenas três:

public: faz com que uma propriedade ou método de uma classe seja acessível de qualquer lugar, seja de dentro da classe, classes derivadas ou de fora da classe.

protected: faz com que uma propriedade ou método de uma classe seja acessível apenas dentro da própria classe em em classes que a estendem (subclasses).

private: faz com que uma propriedade ou método de uma classe seja acessível somente dentro da própria classe. Não é acessível de fora da classe nem de classes que a estendem.

Vamos ver o comportamento de cada um desses modificadores a seguir 😉

Public

Quando definimos uma propriedade ou método como public, estamos dizendo que ele é acessível de qualquer lugar: dentro da classe, de classes derivadas e de instâncias da classe.

É importante ressaltar que quando criamos um método ou uma propriedade na qual não definimos um modificador (como estávamos fazendo anteriormente), o Typescript faz a inferência como public.

Vamos ver um exemplo:

class Animal {
 public nome: string;

 constructor(nome: string) {
 this.nome = nome;
 }

 public falar() {
 console.log('O animal faz um som');
 }
}

const animal = new Animal('Leão');
console.log(animal.nome); // Acessível
animal.falar(); // Acessível

Protected

Quando definimos uma propriedade ou método como protected, estamos dizendo que ele é acessível apenas dentro da própria classe e em classes que a estendem (ou seja, subclasses). 

É importante ressaltar que este modificador não é acessível fora da classe ou dentro de classes derivadas.

Vamos ver um exemplo:

class Animal {
 protected nome: string;

 constructor(nome: string) {
 this.nome = nome;
 }

 protected falar() {
 console.log('O animal faz um som');
 }
}

class Cachorro extends Animal {
 public latir() {
 console.log('O cachorro late');
 this.falar(); // Acesso permitido
 }
}

const cachorro = new Cachorro('Rex');
console.log(cachorro.nome); // Erro: 'nome' é protegido
cachorro.latir(); // Acesso permitido

Private

Quando definimos uma propriedade ou método como private, estamos dizendo que ele é acessível somente dentro da própria classe. Não é acessível de fora da classe nem de classes que a estendem.

É importante ressaltar que ele é utilizado para esconder detalhes da implementação que não devem ser expostos.

Vamos ver um exemplo:

class Animal {
 private nome: string;

 constructor(nome: string) {
 this.nome = nome;
 }

 private falar() {
 console.log('O animal faz um som');
 }

 public fazerSom() {
 this.falar(); // Acesso permitido
 }
}

const animal = new Animal('Elefante');
console.log(animal.nome); // Erro: 'nome' é privado
animal.fazerSom(); // Acesso permitido, chama 'falar' internamente

Métodos Estáticos (static)

Dentro de uma classe, nós podemos ter funcionalidades estáticas, que nada mais são do que métodos e propriedades que podem ser acessados de fora de uma classe, sem a necessidade de instanciar aquela classe.

Ou seja, nós conseguimos acessar as propriedades e métodos de uma classe sem precisar fazer isso:

cont classe = new MinhaClasse();

Bastando apenas que você faça:

MinhaClasse.propriedadeUm;

MinhaClasse.metodoDois;

O que faz com que nossas classes se pareçam muito com os objetos do Javascript 😉

Vamos ver um exemplo real:

class Animal{
 static nome: string = "Baleia Comum";

 static interagir(): void{
 console.log('Eu sou uma Baleia Comum');
 }
}

console.log(Animal.nome);//Baleia Comum
Animal.interagir();

No exemplo acima, criamos uma propriedade e um método usando o comando static, que diz ao Typescript que eles são do tipo estático.

O que nós dá a possibilidade de acessarmos esses métodos e propriedades sem a necessidade de instanciar a classe Animal (new Animal()).

Uma classe pode conter tanto métodos/propriedades estáticos (com static) quanto métodos/propriedades não estáticos (sem static), mas atente-se ao fato de que métodos/propriedades não estáticos não pode ser acessados diretamente, ou seja, eles precisam que a classe esteja instanciada.

Além disso, métodos estáticos não podem chamar diretamente métodos ou acessar propriedades não estáticas da mesma classe. Isso se deve à natureza dos métodos estáticos e não estáticos.

Portanto, podemos dizer que:

Métodos Estáticos: São chamados na classe diretamente, sem a necessidade de instanciar um objeto da classe. Eles não têm acesso direto a propriedades e métodos não estáticos da classe.

Métodos e Propriedades Não Estáticas: São associados a instâncias específicas da classe. Eles só podem ser acessados através de uma instância da classe.

Classes Genéricas

No TypeScript, classes genéricas permitem que você defina classes que podem trabalhar com múltiplos tipos de dados de maneira flexível e reutilizável.

Seu funcionamento segue a mesma lógica das interfaces genéricas que vimos em lições anteriores 🙃

Vejamos um exemplo:

class Caixa<T> {
 private conteudo: T;

 constructor(conteudo: T) {
 this.conteudo = conteudo;
 }

 // Método para obter o conteúdo da caixa
 obterConteudo(): T {
 return this.conteudo;
 }

 // Método para atualizar o conteúdo da caixa
 definirConteudo(conteudo: T): void {
 this.conteudo = conteudo;
 }
}

// Criando instâncias da classe genérica com diferentes tipos
const caixaDeNumero = new Caixa<number>(123);
console.log(caixaDeNumero.obterConteudo()); // Output: 123

const caixaDeString = new Caixa<string>('Olá, TypeScript!');
console.log(caixaDeString.obterConteudo()); // Output: Olá, TypeScript!

Note que no comando acima, nós criamos um tipo genérico chamado de T, que é reutilizado em todos os métodos e propriedades daquela classe (Caixa).

Já no momento da instanciação da própria classe, nós precisamos passar o tipo de dado que o tipo genérico que a classe vai trabalhar, que no primeiro caso foi um number e em seguida uma string:

new Caixa<number>(123);

....

new Caixa<string>('Olá, TypeScript!');

Você também pode usar tipos genéricos em métodos dentro da classe, e não apenas na definição da própria classe. Vejamos um outro exemplo:

class Utilitario {
 // Método genérico
 trocar<T>(valor: T): T {
 return valor;
 }
}

const util = new Utilitario();
console.log(util.trocar<number>(42)); // Output: 42
console.log(util.trocar<string>('Texto')); // Output: Texto

Note que dessa vez instanciamos a classe (Utilitario) sem a necessidade de informar um determinado tipo genérico. Mas a partir do momento em que precisávamos fazer o uso do método trocar, nós passamos o tipo de dado genérico corretamente 😉

util.trocar<number>(42)

....

util.trocar<string>('Texto')

Parameter Properties

Parameter Properties permitem que você declare e inicialize propriedades da classe diretamente na lista de parâmetros do construtor.

Ou seja, em vez de você precisar definir uma propriedade da classe para posteriormente inicializá-la no corpo do seu método construtor. Você poderia fazer isso em uma única linha dentro da própria assinatura do método construtor.

E esse resultado pode ser alcançado usando a seguinte sintaxe:

class NomeDaClasse {
 constructor(
 public propriedade1: Tipo1,
 private propriedade2: Tipo2,
 protected propriedade3: Tipo3
 ) {
 // O corpo do construtor pode estar vazio ou pode conter lógica adicional
 }
}

Aqui, os comandos public, private e protected são modificadores de acesso que também declaram e inicializam as propriedades.

E como você já sabe, esses modificadores controlam o nível de acesso das propriedades fora da classe.

Vejamos agora, um exemplo real da utilização dos parameter properties:

class Pessoa {
 constructor(public nome: string, private idade: number) {}

 mostrarNome() {
 console.log(this.nome);
 }

 mostrarIdade() {
 console.log(this.idade);
 }
}

const pessoa = new Pessoa('João', 30);
pessoa.mostrarNome(); // Output: João
pessoa.mostrarIdade(); // Output: 30

No código acima, é como se aquelas variaveis temporárias que existem dentro do método construtor, se transformassem em propriedades da nossa classe.

O que não deixa de ser considerado uma estratégia mais enxuta de se criar propriedades de uma classe.

Expressões de Classe

Uma expressão de classe ou também conhecida como class expressions, é um recurso do Typescript que é usado para criar uma classe anônima.

Vamos ver um exemplo:

const Animal = class{
 nome: string = "Baleia";

 interagir(): void{
 console.log('Eu sou uma baleia');
 }
}

const baleia = new Animal();
console.log(baleia.nome);
baleia.interagir();



Uma classe anônima nada mais é do que uma classe sem nome que está sendo armazenada dentro de uma variável 😆

Nós também podemos usar tipos genéricos junto com este recurso da seguinte forma:

class Animal<T> {
 nome: T;

 constructor(nome: T) {
 this.nome = nome;
 }

 interagir(): void {
 console.log(`Eu sou um(a) ${this.nome}`);
 }
}

const baleia = new Animal<string>("Baleia");
console.log(baleia.nome);Baleia
baleia.interagir();//Eu sou um(a) Baleia

Fácil, não?

Classes Abstratas

No mundo da orientação a objetos, uma classe abstrata é um conceito na qual uma classe não pode ser instanciada de forma direta.

Em vez disso, ela serve como um modelo para outras classes. Ela pode definir métodos e propriedades que outras classes concretas (não abstratas) devem implementar ou usar.

"Mas esse conceito nós já não vimos em Interfaces?".

Exato, e embora tanto classes abstratas quanto interfaces em TypeScript sirvam para definir contratos e estruturar código, elas têm algumas diferenças importantes.

Classe Abstrata: quando você quer fornecer uma implementação parcial que outras classes possam reutilizar e quando você quer garantir que uma classe derivada siga um modelo específico. Ideal para criar uma hierarquia de classes com alguma lógica compartilhada.

Interface: quando você quer garantir que diferentes classes (que podem não ter uma relação de herança) implementem um determinado conjunto de métodos e propriedades. Ideal para definir contratos que diversas classes podem seguir, promovendo flexibilidade.

Dito isso, vamos seguir e ver um exemplo do uso de uma classe abstrata:

// Definição de uma classe abstrata
abstract class Veiculo {
 // Propriedade comum
 cor: string;

 // Construtor para inicializar a propriedade
 constructor(cor: string) {
 this.cor = cor;
 }

 // Método abstrato que deve ser implementado pelas subclasses
 abstract acelerar(): void;

 // Método concreto que pode ser usado pelas subclasses
 descrever() {
 console.log(`Este veículo é de cor ${this.cor}.`);
 }
}

// Classe concreta que estende a classe abstrata Veiculo
class Carro extends Veiculo {
 // Implementação do método abstrato acelerar
 acelerar() {
 console.log('O carro está acelerando.');
 }
}

// Outra classe concreta que estende a classe abstrata Veiculo
class Moto extends Veiculo {
 // Implementação do método abstrato acelerar
 acelerar() {
 console.log('A moto está acelerando.');
 }
}

// Instanciando as classes concretas
const meuCarro = new Carro('vermelho');
const minhaMoto = new Moto('azul');

// Usando os métodos das instâncias
meuCarro.descrever(); // Saída: Este veículo é de cor vermelho.
meuCarro.acelerar(); // Saída: O carro está acelerando.

minhaMoto.descrever(); // Saída: Este veículo é de cor azul.
minhaMoto.acelerar(); // Saída: A moto está acelerando.

Note que usamos o termo abstract para definir uma classe abstrata (Veiculo) que será herdada (extends) por outras classes (Carro e Moto).

Note também que a classe abstrata implementa mais recursos que nossas interfaces, como é o caso de métodos concretos que já possuem uma implementação pré-determinada, e que podem ser reaproveitados por uma classe 😁 (sem a necessidade de implementação).

Instanciando uma classe por meio de outra

O Typescript possui um recurso muito interessante na qual nos deixa instanciar uma determinada classe por meio de outra, isto é, desde que a classe A possua os mesmos métodos e propriedades da classe B.

Vamos ver um exemplo:

class Animal{
 nome!: string;
}

class Humano{
 nome!: string;
}

const metamorfo: Animal = new Humano();

O Typescript só permite isso, pois ele reconhece que ambas as classes são iguais, não pede seus nomes (Animal e Humano), mas pelo conteúdo estrutural que cada uma delas possui, que nesse caso é o mesmo. Pois ambas possuem às mesma propriedades com os mesmos tipos (nome!: string;).

Arquivos da lição

Os arquivos desta lição podem ser encontrados no repositório do GitHub por meio deste link.

Conclusão

Ufa! Acredito que você aprendeu bastante coisa relacionado a criação e definição de classes no Typéscript, certo?

Sinta-se a vontade para voltar nesta lição sempre quando desejar, seja para tirar algumas dúvidas, ou relembrar alguns conceitos (antes da entrevista de emprego).

O entendimento de classes no Typescript é fundamental para o seu desenvolvimento como desenvolvedor em Typescript.

Até a próxima lição 🤩