Introdução a Interfaces

Introdução a Interfaces

Jornada Javascript: Classes

Apesar dessa lição não estar intimamente ligada a lição de classes, ainda veremos alguns conceitos de classes por aqui, logo, é importante que você saiba trabalhar com classes no Javascript. Link da lição abaixo!

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

Um dos conceitos mais vitáis e de grande importância na sua jornada como desenvolvedor Typescript, são as INTERFACES!

Como você já sabe, o Typescript é uma linguagem TOTALMENTE TIPADA, o que meio que nos obriga a declarar tipos de dados em 99% da nossa lógica.

E como você verá no futuro, seja no React com Typescript, Node com Typescript, NestJS, NextJS, ou Typescript sozinho... diversas aplicações fazem o uso de INTERFACES.

"De todas as aplicações que já participei onde o time estava usando Typescript, 95% delas faziam o uso de declarações de interfaces para organizar o código..."

Mas o que são elas, e pra que elas servem? É isso que iremos descobrir agora 🙂

O que são interfaces no mundo da programação?

No mundo do desenvolvimento de software, uma interface é uma característica da orientação de objetos, na qual define e descreve um conjunto de métodos e propriedades que uma classe ou objeto deve implementar.

É como se definíssemos um contrato dizendo que aquela classe ou aquela função devem conter (sem precisar definir como devem fazer).

Para exemplificar, imagine que você criou uma nova classe chamada de Retangulo:

class Retangulo{
 oQueEuSou(){
 console.log("Eu sou um RETANGULO!");
 }
}

Você concorda comigo que dentro dessa classe, podemos declarar quantos métodos e propriedades nós quisermos?

É claro que concorda rs

Apesar disso, nós podemos ter diversas outras classes dentro da nossa aplicação, como por exemplo:

class Retangulo{
 oQueEuSou(){
 console.log("Eu sou um RETANGULO!");
 }
}

class Triangulo{
 oQueEuSou(){
 console.log("Eu sou um TRIANGULO!");
 }
}

class Circulo{
 oQueEuSou(){
 console.log("Eu sou um CIRCULO!");
 }
}

Olhando para o código acima, o que todas elas tem em comum?

Primeiro, podemos notar que todas elas contém o mesmo método chamado de oQueEuSou().

Segundo, todas elas representam formas geométricas.

Sabendo disso, que tal definirmos um contrato na qual obrigue que todas as classes (e futuras classes) que possuem relação com formas geométricas, precisem implementar certos métodos e propriedades? 

Com o Typescript isso é totalmente possível por meio das INTERFACES!

Observação: Não é possível implementar interfaces em Javascript!

Implementando Interfaces em Typescript

Agora que você já sabe o que é uma interface, vamos aprender a declarar uma:

interface Usuario{
 nome: string,
 idade: number,
 ativo: boolean
}

No comando acima nós estamos declarando uma especie de objeto chamado de usuário por meio do comando interface, que por sua vez, declara três tipos de chaves e seus respectivos valores:

  • nome = string
  • idade = number
  • ativo = boolean

Cada uma dessas variáveis nada mais são do que as propriedades de uma interface. E assim como as cláusulas de um contrato, elas definem o que deve ser implementado dentro de numa classe, uma variável (array ou objeto) ou dentro de uma função.

Observação: o nome de uma interface deve sempre começar com letra maíuscula, ok?

Para definir uma variável a um tipo de interface, nós podemos fazer isso da seguinte forma:

const dadosUsuario: Usuario = {
 nome: "Micilini",
 idade: 28,
 ativo: true
}

No comando acima, nós estamos definindo um objeto dentro de uma variável chamada dadosUsuario que recebe a interface que criamos anteriormente.

Alí dentro nós estamos definindo as propriedades daquele contrato, como o nome, idade e ativo.

Só que aí vem o pulo do gato.... se você experimentar não declarar alguma das propriedades que foram definidas dentro de uma interface, como por exemplo:

const dadosUsuarioDois: Usuario = {
 nome: "Micilini",
 ativo: true
}

//No comando acima nós não declaramos a propriedade 'idade'

Automaticamente, tanto o visual studio code, quanto o compilador do Typescript alegarão um erro dizendo que o tipo idade está faltando:

E isso faz muito sentido, pois se definimos que a nossa variável (dadosUsuarioDois) está atribuída a um contrato (Usuario) que nos obriga a cumprir todas as nossas cláusulas (nome, idade e ativo), é obvio que o juiz (compilador) vai notar inconsistências de modo a não deixar a nossa aplicação rodar rs

Sendo assim, se torna obrigatório a implementação da propriedade idade:

const dadosUsuarioDois: Usuario = {
 nome: "Micilini",
 ativo: true,
 idade: 77
}

Maaaas... ela não precisa seguir a ordem de declaração que definimos acima, basta apenas que você a implemente no seu objeto!

"Será que é possível adicionar cláusulas que não foram declaradas no contrato? Por exemplo: Está declarado que preciso implementar X, Y e Z... mas no final implementei X, Y, Z e E, isso é possível?"

const dadosUsuarioDois: Usuario = {
 nome: "Micilini",
 ativo: true,
 idade: 77,
 id: 9987,
}

Infelizmente isso não é possível no Typescript, pois quando você define uma interface como Usuario com propriedades específicas (nome, idade e ativo), qualquer objeto que você criar utilizando essa interface deve ter exatamente essas propriedades, com os tipos especificados na interface - ao menos que você altere a interface... -.

Se você quer que a sua interface aceite mais tipos, basta adicionar tais propriedades dentro da declaração da mesma, como por exemplo:

interface Usuario {
 nome: string;
 idade: number;
 ativo: boolean;
 id: number; // Adicionando a propriedade id à interface
}

const dadosUsuario: Usuario = {
 nome: "Micilini",
 idade: 28,
 ativo: true,
 id: 998
};

Uma segunda opção é não fazer o uso das interfaces e declarar seus objetos normalmente da seguinte forma:

const dadosUsuario = {
 nome: "Micilini",
 idade: 28,
 ativo: true,
 id: 998
};

Por fim, nós temos uma terceira opção que é um pouco mais complexa, que envolve usar tipos mais genéricos como { [key: string]: any } para permitir qualquer tipo de propriedade. (Nesta, ainda estamos excluíndo o uso de interfaces):

const dadosUsuario: { [key: string]: any } = {
 nome: "Micilini",
 idade: 28,
 ativo: true,
 id: 998
};

Acessando variáveis que implementam interfaces

Como vimos anteriormente, nós declaramos uma variável chamada de dadosUsuario que implementa as propriedades de uma interface:

interface Usuario{
 nome: string,
 idade: number,
 ativo: boolean
}

const dadosUsuario: Usuario = {
 nome: "Micilini",
 idade: 28,
 ativo: true,
}

No caso do exemplo acima, a variável dadosUsuario nada mais é do que um simples objeto:

console.log(dadosUsuario);//{nome: "Micilini", idade: 28, ativo: true}

E que também pode ser acessado informando suas chaves da seguinte forma:

console.log(dadosUsuario.nome);//Micilini
console.log(dadosUsuario.idade);//28
console.log(dadosUsuario.ativo);//True

Implementando interfaces em funções

No Typescript também é possível implementar interfaces em nossas funções da seguinte forma:

function saudarUsuario(usuario: Usuario){
 console.log(`Olá ${usuario.nome}, tudo bem?`)
}

saudarUsuario(dadosUsuario);

Observe agora como podemos retornar uma interface usando uma função:


interface UsuarioDois{
 nome: string,
 idade: number,
 ativo: boolean
}

function criarUsuario(nome: string, idade: number, ativo: boolean): UsuarioDois{
 return {
 nome,
 idade,
 ativo
 }
}

const usuario: UsuarioDois = criarUsuario("Micilini", 28, true);

Incrível, não?

Implementando interfaces em classes

Observe abaixo um exemplo bem divertido onde estamos criando uma classe chamada Cachorro que implementa uma interface chamada Animal:

// Definição da interface Animal
interface Animal {
 nome: string;
 emitirSom(): void;
 movimentar(): void;
}

// Implementação da classe Cachorro que implementa a interface Animal
class Cachorro implements Animal {
 constructor(public nome: string) {}

 emitirSom(): void {
 console.log(`${this.nome} faz au au!`);
 }

 movimentar(): void {
 console.log(`${this.nome} corre.`);
 }

 // Método específico da classe Cachorro
 brincar(): void {
 console.log(`${this.nome} está brincando.`);
 }
}

// Exemplo de uso
const cachorro = new Cachorro("Rex");
cachorro.emitirSom(); // Saída: Rex faz au au!
cachorro.movimentar(); // Saída: Rex corre.
cachorro.brincar(); // Saída: Rex está brincando.

No caso das classes, nós precisamos usar o termo implements para que ela possa extender e implementar o contrato (Animal).

Note que dentro da classe Cachorro eu consegui implementar um novo método chamado de brincar() ao mesmo tempo que ele não existe dentro da interface Animal. (Diferente do que aprendemos anteriormente, como estamos trabalhando com classes, aqui só precisamos cumprir suas claúsulas, mas nada impede que façamos nossas próprias).

Note também que a interface Animal nós pediu para que declarássemos duas funções chamadas de emitirSom que retorna um void, e movimentarque também retorna um void, ao mesmo tempo que ela não define a maneira como isso deve ser implementado.

É como se ela dissesse: "Você precisa implementar um método chamado emitirSom e outro chamado movimentar, onde ambos retornam um void... como você vai criar a lógica eu não sei... o problema é seu 🤪"

Type Alias X Interfaces

Na lição anterior, você aprendeu sobre o Type Alias, e como podemos criar tipos de dados que são atribuídos a outras nomenclaturas.

Talvez você esteja se perguntando: "O Type Alias não chega a ser a mesma coisa que uma Interface?".

A resposta inicial é sim, eles são bem parecidos, mas não são similares 😉

Enquanto às Interfaces podem ser alteradas ao longo do código, com o Type Alias nós não temos essa mesma flexibilidade.

A escolha entre type alias ou interfaces na maioria das vezes, pode ser optada dependendo do projeto. Existem projetos que só trabalham com interfaces, outros somente com types, e ainda existem aqueles que trabalham com os dois.

E falando sobre o fato de podermos alterar uma interface, vamos ver isso na prática 🙃

Alterando uma Interface

Para alterar (adicionar novas propriedades) a uma interface já existente, basta apenas que você refaça a declaração dela, por exemplo:

interface Usuario{
 nome: string,
 idade: number,
 ativo: boolean
}

interface Usuario{
 id: number,
 nome: string,
 idade: number,
 ativo: boolean,
}

Lembrando que quando você altera as propriedades de uma interface, seja em qualquer parte do seu código. Suas funções, classes e variáveis que dependem dela começam a dar problema, quer ver um exemplo?

interface Usuario{
 nome: string,
 idade: number,
 ativo: boolean
}

const dadosUsuario: Usuario = {
 nome: "Micilini",
 idade: 28,
 ativo: true,
}

No código acima eu defini uma interface que contém os atributos nome, idade e ativo,e após isso, eu usei essa interface dentro da variável dadosUsuario.

Supondo que abaixo da declaração dessa variável (dadosUsuario), eu queira modificar essa interface, e adicionar uma nova propriedade chamada id, eu poderia fazer isso da seguinte forma:

interface Usuario{
 nome: string,
 idade: number,
 ativo: boolean
}

const dadosUsuario: Usuario = {
 nome: "Micilini",
 idade: 28,
 ativo: true,
}

interface Usuario{
 id: number,
 nome: string,
 idade: number,
 ativo: boolean,
}

O único problema, é que a partir do momento que eu redeclaro essa interface, o código anterior (dadosUsuario) começa a dar um erro, alegando que a propriedade id não existe.

Portanto, é extremamente recomendado que suas interfaces sejam definidas logo no início do seu arquivo.

Criando propriedades opcionais em nossas interfaces

Também é possível criar propriedades e métodos opcionais dentro das nossas interfaces.

O processo é bem simples, basta apenas que informemos o sinal de interrogação (?) após a declaração de uma propriedade:

interface Usuario{
 id: number,
 nome: string,
 idade: number,
 ativo: boolean,
 telefone?: string//Exemplo de uma propriedade opcional
}

Ou quem sabe, a declaração de um método:

interface Usuario{
 id: number,
 nome: string,
 idade: number,
 ativo: boolean,
 telefone?: string,
 saudar?(): string//Exemplo de um método opcional
}

Tentando modificar o valor de um Type Alias

Assim como você viu na lição passada, um type alias pode atuar de forma bastante similar a uma interface:

type tipoPessoa = {
 nome: string,
 idade: number
}
 
const dadosPessoa: tipoPessoa = {
 nome: "Micilini Roll",
 idade: 23,
}

A única diferença, é que não podemos alterar o valor de um type alias:

type tipoPessoa = {
 nome: string,
 idade: number
}

type tipoPessoa = {
 nome: string,
 idade: number,
 id?: number
}//Isso gera um erro de compilação no Typescript

O que vai de encontro ao que falamos anteriormente nessa lição 😉

Propriedade Readonly

Dentro de uma interface, nós podemos definir nossas propriedades como readonly, o que é uma forma simples de dizer que aquele valor pode ser alterado apenas uma única vez, que é o momento quando criamos um novo objeto.

Veremos como isso funciona na prática:

interface Livro {
 readonly titulo: string;
 autor: string;
 anoPublicacao?: number; // Opcionais
}

const livro: Livro = {
 titulo: 'O Guia do Mochileiro das Galáxias',
 autor: 'Douglas Adams'
};

// livro.titulo = 'Outro Título'; // Erro: Propriedade somente leitura
livro.autor = "Fernando";

No caso do comando acima, o titulo sempre será 'O Guia do Mochileiro das Galáxias'.

O readonly é como se fosse uma constante de um objeto 😉

Usando a Index Signature dentro de uma Interface

No TypeScript, a index signature (assinatura de índice) permite definir tipos para propriedades de um objeto que não são conhecidas de antemão. 

O que nos permite criar objetos onde as chaves são dinâmicas, ou seja, quando você não conhece todos os nomes das propriedades, mas sabe quais são seus tipos.

Isso restringe nossos tipos que não deve ser utilizados, o que garante uma segurança a mais na nossa variável.

Vejamos como isso funciona:

interface NomeDaInterface{
 [index: string]: number
}

let meuObjeto: NomeDaInterface = {
 micilini: 10
}

let meuObjetoDois: NomeDaInterface = {
 roll: 988
}

No comando acima, nós criamos uma interface (NomeDaInterface) que declara suas propriedades de forma customizada, o que permite que os futuros objetos que a utilizem possam definir chaves customizadas (desde que sejam strings) e valores (desde que sejam númericos).

Mas a partir do momento que trocamos o tipo de index para number:

interface NomeDaInterface{
 [index: number]: number
}

Nossos futuros objetos são obrigados a definir valores númericos na chave:

interface NomeDaInterface{
 [index: number]: number
}

let meuObjeto: NomeDaInterface = {
 455: 10
}

let meuObjetoDois: NomeDaInterface = {
 122: 988
}

Além disso, você ainda consegue adicionar propriedades adicionais, observe:

interface NomeDaInterface{
 [index: string]: number
}

let meuObjeto: NomeDaInterface = {
 micilini: 10,
 roll: 65
}

let meuObjetoDois: NomeDaInterface = {
 roll: 988,
 micilini: 1007
}

Heranças com Interfaces

Na programação orientada a objetos, nós temos o conceito de herança, que busca fazer com que determinadas classes possam herdas propriedades e métodos de outras classes;

Aqui nas interfaces, o conceito de heranças não é muito diferente, visto que aqui também fazemos o uso do Extending Types que é comumente usada com o comando extends 🙂

interface Animal{
 nome: string,
 classe: string
}

interface SuperAnimal extends Animal{
 poder: string[],
 rankDeHeroi: number
}

const peixe: SuperAnimal = {
 nome: 'Peixe',
 classe: 'Do Mar',
 poder: ['Respirar fora da agua', 'Conversar igual humano', 'Voar até a extratosfera sem danos'],
 rankDeHeroi: 32
}

console.log(peixe);

Note que criamos uma classe SuperAnimal que extende da classe Animal, e com isso herda (e pode usar) as propriedades alí definidas.

Interseção com Interfaces

Também conhecidos como Intersection Types, eles permitem combinar múltiplos tipos em um único tipo de Interface.

O que combina todas as propriedades e métodos dos tipos envolvidos, se tornando extramemente útil quando você deseja criar um tipo que contenha todas as características de um ou mais tipos diferentes.

A sintaxe é a seguinte:

type TipoA = { a: string };
type TipoB = { b: number };

type TipoC = TipoA & TipoB;//Usamos o & para concatenar dois objetos diferentes

Neste exemplo, TipoC é um tipo de interseção que combina TipoA e TipoB. Assim, TipoC terá todas as propriedades de TipoA e TipoB.

Agora veremos um exemplo prático utilizando Interfaces:

// Definindo dois tipos
type Humano = {
 nome: string;
 idade: number;
};

type Trabalhador = {
 cargo: string;
 salario: number;
};

// Criando um tipo de interseção
type Funcionario = Humano & Trabalhador;

// Criando um objeto que corresponde ao tipo Funcionario
const funcionario: Funcionario = {
 nome: 'Maria',
 idade: 30,
 cargo: 'Desenvolvedora',
 salario: 5000
};

console.log(funcionario);

Simples, não?

Arquivos da lição

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

Conclusão

Nesta lição você aprendeu sobre o uso das interfaces no Typescript.

Em aulas futuras, nós veremos muitos exemplos de interfaces em conjunto com bibliotecas 😇

Até a próxima!