Typescript - Uma breve introdução - Interfaces

09 de agosto de 2020  | 
7 min de leitura

Nos artigos anteriores a gente viu um pouco sobre tipos básicos, enums e type assertions. Recomendo dar uma conferida neles, caso não se sinta confortável com esses conceitos.

Em quase todos os cenários apenas os tipos básicos não vão ser suficientes para que consigamos escrever uma aplicação robusta usando typescript, pois a medida que os nossos dados ficam mais complexos as tipagem tendem a seguir o mesmo caminho. É pensando nesse cenário que o typescript nos oferece as interfaces para a criação de tipos customizados.

Duck typing

Antes de prosseguir preciso explicar um pouco sobre Duck typing(tipagem pato), pois é um conceito importante para entender o funcionamento de interfaces.

O uso do duck typing ou structural subtyping(tipagem estrutural) foca exatamente na forma que os valores apresentam, ela segue o princípio de que se anda como um pato, voa como um pato e grasna como um pato então é um pato. O nome desse conceito é associado ao teste do pato de James Whitcomb Riley.

Suponha que você vê um pássaro andar por volta de uma fazenda. Este pássaro não tem nenhuma placa que diz 'pato'. Mas o pássaro certamente parece a um pato. Também, ele vai ao tanque e você nota que ele nada como um pato. Então ele abre o seu bico e grasna como um pato. Bem, agora você conseguiu provavelmente a conclusão que o pássaro é um pato, mesmo que ele esteja usando uma placa ou não Harv,Immerman,1982, p.102

Isso pode não ter feito muito sentido pra você, mas siga lendo o textinho que logo vai ficar claro como isso se comporta em termos de código.

Criando nossa primeira interface

Pense que temos uma função que recebe um usuário como argumento e esse usuário precisa ter sempre nome e idade. A função ficaria assim:

function showUser(user: {name: string, age: number}) {
  console.log(user.name + 'tem ' + user.age + ' anos de idade');
}

showUser({name: 'Cristiano', age: 25});

Agora imagine que precisamos fazer operações com user em várias partes do nosso código e também precisamos garantir que ele sempre tenha name e age. A gente poderia simplesmente sair reescrevendo o objeto {name: string, age: number} em todo lugar que a gente precisar ou então poderíamos extrair isso em uma interface:

interface User {
  name: string;
  age: number;
}

function showUser(user: User) {
  console.log(user.name + 'tem ' + user.age + ' anos de idade');
}

const newUser: User = {
  name: 'Cristiano',
  age: 25
};   

Tá, mas cadê o duck typing?

Calma... Vamos supor que temos uma função que recebe o nome e o tipo de um pokemon e imprime um dos seus counters.

interface Pokemon {name: string, type: 'fogo' | 'agua' | 'grama'}

function showPokemonCounter(pokemon: Pokemon) {
  const counters = {
    fogo: 'agua',
    agua: 'grama',
    grama: 'fogo'
  }
  
  console.log(pokemon.name + ' é fraco contra ' + counters[pokemon.type]);
  
}

const charmander = {name: 'charmander', type: 'fogo'};
showPokemonCounter(charmander);  // charmander é fraco contra agua 

O typescript verifica se o argumento passado para a função showPokemonCounter é exatamente um objeto que possuí uma propriedade name do tipo string e uma propriedade type do tipo 'fogo', 'agua' ou 'grama'.

Até aqui tudo bem... E agora que entra o duck typing. O que acontece se eu adicionar mais uma propriedade no argumento que vou passar para showPokemonCounter ?

interface FullPokemon {name: string, type: 'fogo' | 'agua' | 'grama', pokeNumber: number}

const squirtle: FullPokemon = {name: 'squirtle', type: 'agua', number: 7};
showPokemonCounter(squirtle); // squirtle é fraco contra grama 

Opa... Nenhum erro aconteceu! Isso graças a ação do duck typing, pois mesmo tendo a propriedade number, o pokemon que passamos ainda tem o name e o type exatamente como a função esperava.

É como se a gente tivesse um pato que voa, nada, grasna e fica invisível. Ele ta fazendo uma coisa diferente..., mas ele ainda faz as coisas que definimos como essenciais para classificá-lo como um pato.

Interfaces híbridas

Até aqui você já deve ter percebido que as interfaces podem ser compostas por diversos tipos. Number, strings, booleans, functions, etc e até tipos customizados.

interface Addres {
 city: string;
  country: string;
}

interface User {
  name: string;
  address: Addres;
  sayHi():string;
}

const user: User = {
  name: 'Cris',
  address: {city: 'Alagoinhas', country: 'Brasil'},
  sayHi: () => 'olá'
}

Propriedades opcionais

Podem existir casos em que algumas propriedades podem existir ou não e é muito fácil de definir isso, basta adicionar um ? depois do nome da propriedade. Só é preciso tomar cuidado, pois uma propriedade não definida tem o seu valor undefined.

interface User {
  name: string;
  age?: number;
}

const user = User = {name: 'Cris'};
user.name // Cris
user.age // undefined

Propriedade de apenas leitura

Para casos em que uma propriedade não pode ser alterada depois da sua definição a gente pode usar o readonly.

interface User {
 readonly id: number;
  name: string;
  age?: number;
}

const user: User = {id: 1, name: 'Cris'};

user.name = 'Cristiano' // tudo ok
user.id = 2 // Erro! Cannot assign to 'id' because it is a read-only property.

Tipando funções

As interfaces permitem tipar objetos de diversas formas, mas elas também são ótimas para descrever tipos de funções.

interface SumType {
  (a: number, b: number): number;
}

const sum: SumType = (a, b) => a + b;
sum('a', 2) // Erro! Argument of type '"a"' is not assignable to parameter of type 'number'.

sum(2, 2) // 4

Estendendo interfaces

Assim como classes, as interfaces podem ser estendidas e isso permite que a gente crie interfaces genéricas e reaproveitáveis.

interface User {
  name: string;
  email: string;
}

interface Admin extends User {
  isAdmin: boolean;
}

Interfaces com tipos Indexáveis

Os tipos indexáveis tem uma assinatura de índice que descreve os tipos que podemos usar para indexar em um objeto, exemplo a[10] (usa um número como índice) a['name'](usa string como índice).

interface NumberOrStringDictionary {
  [key: number]: number | string;
}

const array: NumberOrStringDictionary = [1, 2, 'a']

console.log(array[1]); // 2
console.log(array['2']); // Erro

É interessante que se tentarmos fazer um array.push('b') vamos ter um erro de tipo, pois push não foi definido na interface NumberOrStringDictionary. Teríamos que fazer algo assim:

interface NumberOrStringDictionary {
  [key: number]: number | string;
  push(val: number | string): number;
}

const array: NumberOrStringDictionary = [1, 2, 'a']
array.push('b')
console.log(array[3]); // b

Também podemos usar tipos indexáveis para trabalhar com objetos ou array de objetos que as propriedades são dinâmicas. Eu costumo usar isso com uma certa constância.

interface DynamicObject {
  [key: string]: number;
}


const array: DynamicObject[] = [{'one': 1, 'two': 2}, {'three': 3}, {'four': 4}]

console.log(array) // [ { "one": 1, "two": 2 }, { "three": 3 }, { "four": 4 } ] 

Devo usar classes ou interfaces

Existe a possibilidade de tipar nossos dados utilizando classes, porém quando nosso typescript é transformado em javascript a gente acaba tendo uma classe inútil, pois ela é declarada e não é usada em local nenhum. Isso acontece porque o typescript só fazia uso em tempo de execução. Veja as imagens abaixo.

Tipagem usando interfaces
Tipagem usando interfaces

A imagem acima mostra o resultado de um código javascript tipado usando interfaces, note que a interface foi descartada e não poluí o nosso código final.

Tipagem usando classes
Tipagem usando classes

A imagem acima mostra um código javascript tipado usando classes, note que uma classe é criada, mas não é usada em momento algum no nosso código final.
PS: eu precisei iniciar as propriedades com um valor, pois o typescript reclamou que elas não tinham sido inicializadas no construtor.

Acredito que ficou claro qual opção escolher quando queremos apenas ter a segurança de tipos.

Isso é tudo pessoal!

Isso é tudo pessoal

Obrigado por chegar até aqui!! Espero que tenha conseguido te ajudar de alguma forma. =]

Em breve irei escrever mais conteúdo sobre Typescript.

Então... Até mais!

Links importantes

Comentários