Conheça os novos tipos de dados do JavaScript

Lucas Santos - Mar 31 '22 - - Dev Community

Se você acompanha a lista de propostas do JavaScript no repositório do TC39, provavelmente já deve ter se deparado com as propostas mais novas para a linguagem.

Se você ainda não sabe o que é o TC39 ou como o JavaScript funciona, eu fiz um vídeo super bacana sobre isso que você pode assistir aqui.

O modelo de evolução do JavaScript é extremamente importante para a linguagem porque permite que qualquer pessoa inclua sua própria proposta e sugira modificações e adições para a linguagem, basta ter um caso de uso bom e convencer a maioria dos champions!

Uma das propostas que está ganhando um pouco de tração é a adição de dois novos primitivos chamados Tuple e Record. E eles vão fazer toda a diferença para quem usar.

Sobre imutabilidade

Records e Tuples não são novos na programação, outras linguagens já usam esse tipo de primitivo para poder representar valores que chamamos de coleções. Assim como Arrays e Objetos, uma Tuple (ou tupla em potuguês) ou um Record são também conjuntos de valores agrupados em um único endereço de memória.

A diferença desses primitivos para os primitivos que já temos, como o Array e o Objeto, é que eles são imutáveis.

Você pode definir uma Tupla como:

let tuple = #['minha', 'tupla']

let tupla = Tuple(['um', 'array'])

Enter fullscreen mode Exit fullscreen mode

Nós podemos também definir uma tupla a partir de um outro array:

const tupla = Tuple(...[1, 2, false, true])
const tuple = Tuple.from([false, true, 'a'])

Enter fullscreen mode Exit fullscreen mode

Já os records são as variantes de objetos das tuplas e podem ser definidos como:

let record = #{
  meu: 'novo',
  record: true
}

let outroRecord = Record({ um: 'objeto' })

Enter fullscreen mode Exit fullscreen mode

Imutabilidade é uma característica cada vez mais comum em grande parte dos sistemas construidos atualmente, mas, assim como as coleções, ela já vem de muito tempo atrás.

A ideia de criar um objeto imutável é que ele, como o próprio nome já diz, ele não sofra nenhum tipo de alteração ao longo da sua vida, mas isso não significa que você nunca mais vai poder alterar a variável depois de criada, o que acontece é que o valor original dela não é mudado.

Na prática, uma variável imutável iria criar uma cópia de si a cada operação que é feita sobre ela. Nós já temos alguns tipos de imutabilidade no JavaScript com funções como o map, slice, find, filter, reduce e algumas outras. Então, por exemplo, se tivéssemos uma string, e um método para alterar essa string, se ela não fosse imutável teríamos o seguinte resultado:

let string = 'mutavel'
console.log(string) // mutavel

string.mudar('outro valor')
console.log(string) // outro valor

Enter fullscreen mode Exit fullscreen mode

No entanto, se tivermos uma string imutável , vamos ter o seguinte fluxo:

let string = 'imutavel'
console.log(string) // imutavel

let novaString = string.mudar('outro valor') // retorna uma nova string

console.log(string) // imutavel
console.log(novaString) // outro valor

Enter fullscreen mode Exit fullscreen mode

Se, ao invés de uma string, o valor fosse um Array, para cada novo item deste array teríamos um novo array sendo retornado. Isso pode ser entendido facilmente se você pensar que a função slice do Array retorna um novo array que é um subconjunto do array original.

Bibliotecas como o ImmutableJS fazem esse trabalho muito bem. E a grande vantagem da imutabilidade é justamente que você tem um constrole muito maior da sua aplicação por ter um controle completo de todas as etapas do fluxo de dados, de forma que você pode retornar para qualquer valor anterior a qualquer momento.

Claro que isso tem um custo, cada nova versão da sua variável é um espaço extra que vai ser ocupado na memória, se você não remover seus estados anteriores, pode acabar tendo alguns problemas de performance.

Coleções imutáveis

Até aqui tudo bem, mas qual é a grande ideia de falar tanto sobre imutabilidade quando o assunto do post é sobre duas novas coleções? Porque esse fator faz total diferença quando estamos falando de objetos e arrays, principalmente no JavaScript.

Tuplas e Records funcionam da mesma forma que Arrays ou Objetos normais, a maior diferença é que não temos os operadores de alteração "in place", ou seja, as funções que alteram o próprio valor original, como o Array.push ou Array.splice. Se tentarmos criar uma tupla e modificarmos esse valor, ou um record e tentarmos fazer o mesmo, vamos ter um erro:

let record = #{
  nome: 'Lucas'
}

record.idade = 26 // Erro

let tupla = #[1, 2, 3]
tupla[0] = 2 // erro

Enter fullscreen mode Exit fullscreen mode

Comparação por valor

Um dos maiores problemas que recebo como dúvidas de várias pessoas ao longo dos anos é o fato de que o JavaScript compara objetos e arrays como referências, isso já foi explicado rapidamente em um artigo que publiquei no sobre protótipos e herança.

A ideia é que quando comparamos dois objetos ou dois arrays (ou até mesmo outras estruturas que acabam sendo convertidas no tipo de objeto), vamos sempre ter um false como resposta:

console.log({ a: 1 } === { a: 1 }) // false
console.log(['a'] === ['a']) // false

Enter fullscreen mode Exit fullscreen mode

Muita gente acha que esse comportamento é um erro da linguagem e que deveria ser resolvido se usarmos a comparação simples, com == ao invés de ===. Mas o problema não são os tipos, e sim a referência.

Para o JavaScript, dois objetos ou arrays são iguais se apontam para a mesma referência de memória, o que nunca é possível quando comparamos dois objetos literais como esses, porque cada vez que criamos um novo objeto, temos um novo objeto criado e, portanto, um novo endereço de memória, e então não vamos nunca ter uma comparação verdadeira.

Nesse ponto poderíamos até dizer que o JavaScript faz com que a criação de objetos seja, de fato, imutável.

E é aí que entra uma das funcionalidades mais importantes e mais úteis dessas novas primitivas: Tuples e Records são comparados por valores.

Como estamos tratando de conteúdos que são imutáveis, o JavaScript agora pode comparar naturalmente os dois objetos diretamente por valor, isso significa que podemos comparar algo como:

#{a:1} === #{a:1} // true
#[1, 2, 3] === #[1, 2, 3] // true

Enter fullscreen mode Exit fullscreen mode

Isso torna todo o processo de comparação de objetos muito mais fácil ao invés de precisarmos comparar objetos por sua representação de textos com o clássico JSON.stringify.

Manipulando Tuples e Records

Como expliquei antes, as tuplas e records tem exatamente os mesmos métodos de objetos e arrays, a diferença é que não vamos poder adicionar novos valores ou modificar valores existentes, então métodos como o push não existem nesse contexto, no entanto, é possível manipular e até estender os valores desses objetos de forma muito mais fácil.

Podemos utilizar o modificador rest tanto em tuplas como objetos, para poder criar uma nova instância desses valores sem modificar o anterior, isso permite adicionar e modificar valores em tempo real sem precisar escrever tanto. Por exemplo, se tivermos um record como:

const record = #{
  nome: 'Lucas'
}

Enter fullscreen mode Exit fullscreen mode

E agora queremos adicionar a propriedade idade, podemos fazer assim:

const record = #{
  nome: 'Lucas'
}

const recordComIdade = #{
  ...record,
  idade: 26
}

Enter fullscreen mode Exit fullscreen mode

Ou seja, da mesma forma que fazemos com objetos naturalmente, porém de modo assíncrono.

O mesmo vale para as tuplas:

const tuple = #[1, 2, 3]
const tupleComMaisValores = #[...tuple, 4, 5]

Enter fullscreen mode Exit fullscreen mode

A diferença é que as tuplas tem um método a mais, o with, que permite que adicionemos (ou concatenemos) valores no final da tupla:

const tuple = #[1, 2, 3]
const tupleComMaisValores = tuple.with(4, 5) // mesmo resultado do anterior

Enter fullscreen mode Exit fullscreen mode

E, apenas para deixar ainda mais claro, podemos trabalhar com qualquer um desses novos objetos como se fossem arrays ou objetos normais, podemos até esquecer que eles são um tipo novo:

const chaves = Object.keys(#{ name: 'Lucas', age: 26 }) // ['name', 'age']
const tuple = #[1,2,3,4,5]

for (const i of tuple) {
  console.log(i % 2 === 0 ? 'par' : 'impar')
}

Enter fullscreen mode Exit fullscreen mode

Como eu posso começar a usar?

Essa proposta ainda está no estágio 2, isso significa que ela é relativamente estável e tem uma implementação funcional, porém ainda não é considerada uma implementação oficial. Portanto, ela ainda não está presente em nenhum dos maiores players do mercado como, por exemplo, o Node.js e browsers como Mozilla Firefox, Chrome e Edge.

Porém, parte do processo de ser uma proposta de estágio 2 é que ela precisa ter um polyfill (uma implementação "falsa" que imita a funcionalidade na totalidade usando recursos já presentes na linguagem) funcional. Então você pode usar esse polyfill e começar a testar a funcionalidade agora mesmo!

Conclusão

A proposta ainda está em construção, tanto é que existe uma issue aberta desde 2019 para poder definir se a criação das tuplas e records vão ser através de keywords como immutable ou fixed, ou então através de objetos literais, como foi explicado acima.

Além disso, as keywords tuple e record já existem em sistemas de tipos como o TypeScript, e podem ter algum tipo de conflito, que está também sendo discutido desde 2020.

A ideia final é que tudo isso ainda é muito incipiente, mas a proposta está chegando perto de uma conclusão e você pode ajudar a estabelecer o próximo tipo de dados do JavaScript!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .