Explicando a coerção de tipos em Javascript

Saiba como funciona

Allan Ramos
12 min readMay 1, 2019
Coisas estranhas podem acontecer no Javascript

Esse texto é uma tradução livre do artigo JavaScript type coercion explained de Alexey Samoshkin.

[Edit 02/05/2018]: Esse post está disponível em Russo. Palmas a Serj Bulavyk por seus esforços.

Coerção de tipos (type coercion) é o processo de conversão de um valor de um tipo, para outro (como a conversão de uma string para um número, um objeto para um booleano e etc). Qualquer tipo, seja primitivo ou um objeto, é um sujeito válido para coerção de tipo. Para recordar, os primitivos são: number, string, booleano, null, undefined + Symbol (adicionado no ES6).

Como um exemplo de coerção de tipo em prática, veja o link JavaScript Comparison Table, que mostra como o operador de igualdade == se comporta para diferentes tipos. Essa matriz parece assustadora devido à coerção de tipo implícita que o operador == faz, e dificilmente será possível lembrar de todas essas combinações. E você não precisa fazer isso — apenas aprenda os princípios básicos da coerção de tipos.

Esse artigo vai a fundo de como a coerção de tipos funciona no Javascript, e irá prepará-lo com o conhecimento básico para que você possa sentir-se confiante ao explicar sobre as expressões. Ao final do artigo, mostrarei as respostas e as explicarei.

true + false
12 / "6"
"number" + 15 + 3
15 + 3 + "number"
[1] > null
"foo" + + "bar"
'true' == true
false == 'false'
null == ''
!!"false" == !!"true"
[‘x’] == ‘x’
[] + null + 1
[1,2,3] == [1,2,3]
{}+[]+{}+[1]
!+[]+[]+![]
new Date(0) - 0
new Date(0) + 0

Sim, essa é uma lista boba de coisas que você pode fazer como desenvolvedor. Em 90% dos casos é melhor evitar a coerção de tipos implícita. Considere essa lista como exercícios para aprendizagem para testar seu conhecimento de como a coerção de tipos funciona. Se estiver entendiado, você pode encontrar mais em wtfjs.com.

A propósito, você pode encontrar perguntas disso em entrevistas para vagas de Javascript. Então continue lendo 😄.

Coerção Implícita vs Explícita

Coerção de tipos pode ser explícita ou implícita.

Quando um desenvolvedor deseja converter um tipo escrevendo algo como, Number(valor), isso é chamado de coerção de tipos explícita (explicit type coercion ou type casting.

Já que o Javascript é uma linguagem fracamente tipada (weakly-typed language), valores também podem ser convertidos entre diferentes tipos automaticamente, e isso é chamado de coerção de tipos implícita (implicit type coercion). Isso acontece quando você atribui operados para valores de diferentes tipos, como 1 == null, 2/’5', null + new Date(), ou isso pode decorrer do contexto, como usar if (value) {…}, onde value é forçado a retornar um booleano.

Um operador que não desencadeia a coerção de tipos implítica é ===, que é chamado de operador restrito de igualdade (strict equality operator). O operador de igualdade == por outro lado, faz a comparação e ativa a coerção de tipos, se necessário.

Coerção de tipo implícito é uma faca de dois gumes: é uma grande fonte de frustração e defeitos, mas também um mecanismo útil que nos permite escrever menos código sem perder a legibilidade.

Três tipos de conversão

A primeira regra que precisamos saber é que existem apenas 3 tipos de conversão no Javascript:

  • para string;
  • para boolean;
  • para number.

A segunda, é que a lógica para conversão de tipos primitivos e objetos funcionam de forma diferente, mas ambos só podem ser convertido nessas 3 maneiras.

Vamos começar primeiro com os primitivos.

Conversão de String

Para indicar a conversão explícita de valores para string use a função String(). A coerção implícita é ativada pelo operador binário +, quando qualquer operando é uma string:

String(123) // explícito
123 + '' // implícito

Todos os valores primitivos são convertidos em strings naturalmente, como você poderia esperar:

String(123)                   // '123'
String(-12.3) // '-12.3'
String(null) // 'null'
String(undefined) // 'undefined'
String(true) // 'true'
String(false) // 'false'

A conversão de Symbol é um pouco complicada, porque só pode ser convertida explicitamente, mas não implicitamente. Leia mais nas regras de coerção de tipos do Symbol.

String(Symbol('my symbol'))   // 'Symbol(my symbol)'
'' + Symbol('my symbol') // TypeError é lançado

Conversão de Boolean

Para indicar a conversão explícita de valores para boolean use a função Boolean(). A conversão implícita ocorre no contexto lógico ou é ativada por operadores lógicos (|| && !) .

Boolean(2)          // explícito
if (2) { ... } // implícito devido ao contexto lógico
!!2 // implícito devido ao operador lógico
2 || 'hello' // implícito devido ao operador lógico

Observação: Operadores lógicos como || e && fazem conversões booleanas internamente, mas na verdade retornam o valor dos operandos originais, mesmo que eles não sejam booleanos.

// retorna o número 123 ao invés de true
// 'hello' e 123 são convertidos para boolean internamente para calcular a expressão
let x = 'hello' && 123; //x === 123 é true

Assim que houver apenas dois resultados possíveis da conversão booleana: trueou false, é mais fácil lembrar a lista de valores falsos (falsy values).

Boolean('')           // false
Boolean(0) // false
Boolean(-0) // false
Boolean(NaN) // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(false) // false

Qualquer valor não inserido nessa lista ao ser convertido será true, incluindo objetos, funções, Array, Date, tipos definidos pelo usuário e assim por diante. Symbols são considerados como valores verdadeiros (truthy values). Objetos vazios e arrays também:

Boolean({})             // true
Boolean([]) // true
Boolean(Symbol()) // true
!!Symbol() // true
Boolean(function() {}) // true

Conversão Numérica

Para uma conversão explícita aplique a função Number(), assim como feito com Boolean() eString().

A conversão implícita é complicada, pois é acionada em mais casos:

  • operadores de comparação (comparison operators)(>, <, <=,>=)
  • operadores bitwise ( | & ^ ~)
  • operadores aritméticos (- + * / % ). Saiba que usar + não irá ativar a conversão numérica quando qualquer operando for uma string.
  • operador unário+
  • operador de igualdade== (incl. !=).
    Perceba que == não ativa a conversão numérica quando ambos operandos são strings.
Number('123')   // explícito
+'123' // implícito
123 != '456' // implícito
4 > '5' // implícito
5/null // implícito
true | 0 // implícito

Abaixo como valores primitivos são convertido para números:

Number(null)                   // 0
Number(undefined) // NaN
Number(true) // 1
Number(false) // 0
Number(" 12 ") // 12
Number("-12.34") // -12.34
Number("\n") // 0
Number(" 12s ") // NaN
Number(123) // 123

Ao converter uma string em número, a engine primeiro remove os espaços em branco com os caracteres \n e \t, retornando NaN se a string tratada não representar um número válido. Se a string estiver vazia, retornará 0.

null e undefined são tratados de forma diferentes: null vira 0, enquanto undefined se torna NaN.

Symbols não podem ser convertidos em números nem explicitamente nem implicitamente. Além disse, TypeError é lançado ao invés de silenciosamente converter para NaN, como acontece para undefined. Veja mais sobre as regras de conversão de símbolo no MDN.

Number(Symbol('my symbol'))    // TypeError é lançado
+Symbol('123') // TypeError é lançado

Existem duas regras especiais para relembrar:

  1. Quando aplicamos == para null ou undefined, a conversão numérico não ocorre. null é apenas igual a null ou undefined, e não é igual a mais nada.
null == 0               // false, null is not converted to 0
null == null // true
undefined == undefined // true
null == undefined // true

2. NaNnão é igual a nada que não seja ele mesmo:

if (value !== value) { console.log("we're dealing with NaN here") }

Coerção de tipos para objetos

Até agora, analisamos a coerção de tipos para valores primitivos. Isso não é muito empolgante.

Quando isso ocorre com objetos, e a engine encontra expressões como [1] + [2,3], primeiramente será preciso converter o objeto para um valor primitivo, que é então convertido pro tipo final. E ainda assim existem apenas três tipos de conversão: numérico, string e booleano.

O caso mais simples é a conversão para booleano: qualquer valor não primitivo sempre será convertido para true, não importa se um objeto ou array está vazio ou não.

Objetos são convertidos para primitivos através da função [[ToPrimitive]], que é responsável pela conversão numérica e string.

Abaixo uma pseudo implementação do método [[ToPrimitive]]:

[[ToPrimitive]] é invocado passando dois argumentos:

  • input: valor a ser convertido;
  • preferredType: Tipo escolhido para conversão, podendo ser Number ou String. Esse argumento é opcional.

Ambas conversões, número e string fazem uso de dois métodos do objeto de entrada (input): valueOf e toString. Ambas funções são declaradas no Object.prototypee, portanto, disponível para qualquer tipo derivado, como Date, Array, e etc.

Em geral, o algoritmo é o seguinte:

  1. Se o input já é do tipo primitivo, retorne-o;

2. Chame a funçãoinput.toString(), se o resultado for do tipo primitivo, retorne-o;

3. Chame a função input.valueOf(), se o resultado for do tipo primitivo, retorne-o;

4. Se nem a funçãoinput.toString() ou input.valueOf() retornar um tipo primitivo, lance TypeError.

Conversões numéricas primeiro chamam a função valueOf (3) com o fallback toString (2).

A conversão de string faz exatamente o oposto: toString (2) seguido de valueOf (3).

A maioria dos tipos internos(built-in) não possui a função valueOf, ou possui valueOf retornando o próprio objeto, então é ignorado por não ser do tipo primitivo. É por isso que a conversão de tipos numbere string podem funcionar da mesma forma — ambos acabam chamando toString().

Operadores diferentes podem acionar a conversão numérica ou de string com a ajuda do parâmetro preferredType. Mas existem duas exceções: o comparador de igualdade abstrato == e a opção binária + acionam modos de conversão padrão (preferredType não é especificado, ou igual a default). Nesse caso, a maior dos tipos internos(built-in) assumirão uma conversão numérica como default, exceto Date que fará uma conversão de string.

Segue abaixo um exemplo de como se comporta uma conversa de Date:

Você pode sobrescrever os métodos padrãotoString() e valueOf() para conectar-se à lógica de conversão objeto para primitivo(object-to-primitive).

Observe como obj + ‘’ retorna '101' como uma string. O operador + dispara um modo de conversão padrão, e como dito anteriormente, Object assume a conversão numérico como padrão, usando portanto, o método valueOf() ao invés do toString().

Método do ES6 - Symbol.toPrimitive

No ES5 você pode conectar a lógica de conversão de objeto a primitivo(object-to-primitive) substituindo os métodos toString e valueOf.

No ES6 você pode ir mais longe, podendo substituir completamente a rotina interna [[ToPrimitive]] implementando o método [Symbol.toPrimtive]em um objeto.

Exemplos

Sabendo a teoria, agora vamos aos exemplos:

true + false             // 1
12 / "6" // 2
"number" + 15 + 3 // 'number153'
15 + 3 + "number" // '18number'
[1] > null // true
"foo" + + "bar" // 'fooNaN'
'true' == true // false
false == 'false' // false
null == '' // false
!!"false" == !!"true" // true
['x'] == 'x' // true
[] + null + 1 // 'null1'
[1,2,3] == [1,2,3] // false
{}+[]+{}+[1] // '0[object Object]1'
!+[]+[]+![] // 'truefalse'
new Date(0) - 0 // 0
new Date(0) + 0 // 'Thu Jan 01 1970 02:00:00(EET)0'

Abaixo, você encontrará explicações para cada expressão.

O operador binário + aciona a conversão numérica gerando o resultado true ou false.

true + false
==> 1 + 0
==> 1

O operador aritmético / aciona a conversão numérico para a string '6':

12 / '6'
==> 12 / 6
==>> 2

O operador + possui uma associação de leitura a partir da esquerda para a direita (left-to-right associativity), portanto a expressão "number" + 15 é executada primeiro. Desde que o primeiro operando é uma string, o operador + aciona a conversão para string do número 15. No segundo passo, a expressão "number15" + 3 é tratada da mesma forma.

“number” + 15 + 3 
==> "number15" + 3
==> "number153"

A expressão 15 + 3 é avaliada primeiro. Já que ambos operandos são numéricos, não é preciso fazer a coerção dos tipos. Mas na segunda expressão, quando 18 + 'number' é avalido, ao verificar que um dos operandos é uma string, ele aciona a conversão para string.

15 + 3 + "number" 
==> 18 + "number"
==> "18number"

O operador de comparação > acionada a conversão numérica para [1] e null.

[1] > null
==> '1' > 0
==> 1 > 0
==> true

O operador unário + tem maior precedência ao operador binário +. Então a expressão +'bar' é avaliada primeiro. O operador unário aciona a conversão numérica para a string 'bar'. Já que a string não apresenta um número válido, o resultado será NaN. Na segunda etapa, a expressão 'foo' + NaN será avaliada.

"foo" + + "bar" 
==> "foo" + (+"bar")
==> "foo" + NaN
==> "fooNaN"

O operador == aciona a conversão numérica, a string true é convertida para NaN, o booleano true é convertido para 1.

'true' == true
==> NaN == 1
==> false
false == 'false'
==> 0 == NaN
==> false

O operador == normalmente aciona a conversão numérica, mas não é o caso quando é colocado null. null é igual apenas a null ou undefined.

null == ''
==> false

O operador !! converter ambas strings 'true' e 'false' para o booleano true, já que eles não são strings vazias. Então, == apenas verifica a igualdade de dois booleanos true sem qualquer coerção.

!!"false" == !!"true"  
==> true == true
==> true

O operador == aciona a conversão numérica para um array. O método do array valueOf() retorna o próprio array, e é ignorado por não ser um primitivo. A função do array toString() converte ['x'] para a string'x'.

['x'] == 'x'  
==> 'x' == 'x'
==> true

O operador + aciona uma conversão numérica para []. A função do array valueOf() é ignorado, pois retorna a si mesmo, cujo valor não é primitivo. A função do array toString() retorna uma string vazia.

Na segunda expressão '' + null + 1 é avaliada.

[] + null + 1  
==> '' + null + 1
==> 'null' + 1
==> 'null1'

Os operadores lógicos || e && fazem coerção para booleano, mas retornando os operandos originais — não valores booleanos. 0 é falso(falsy), enquanto '0' é verdadeiro(truthy), pois não é uma string vazia. Um objeto vazio {} também retorna verdadeiro(truthy).

0 || "0" && {}  
==> (0 || "0") && {}
==> (false || true) && true // internamente
==> "0" && {}
==> true && true // internamente
==> {}

Não é preciso fazer coerção pois ambos operandos são do mesmo tipo. Desde que == verifica a identidade do objeto (object identity), e não sua igualdade (object equality), o resultado será false, por conta dos 2 arrays serem de instâncias diferentes.

[1,2,3] == [1,2,3]
==> false

Todos os operandos são valores não primitivos, portanto, +inicia a conversão numérica com o item mais a esquerda. A função valueOf de ambos objetos e arrays retornarão a si mesmo, e serão ignorados. O método toString() é usado como fallback. A pegadinha aqui é que {} não é considerado um objeto literal, mas sim como um bloco de declaração de estado, então é ignorado. A avaliação começará com a próxima expressão + [], que será convertido para uma string vazia através do método toString(), e então para 0.

{}+[]+{}+[1]
==> +[]+{}+[1]
==> 0 + {} + [1]
==> 0 + '[object Object]' + [1]
==> '0[object Object]' + [1]
==> '0[object Object]' + '1'
==> '0[object Object]1'

Esse é mais fácil de explicar, pois o passo a passo de sua resolução se dará de acordo com a precedência do operador.

!+[]+[]+![]  
==> (!+[]) + [] + (![])
==> !0 + [] + false
==> true + [] + false
==> true + '' + false
==> 'truefalse'

O operador - acionará a conversão numérica para Date. A função Date.valueOf() retornará o número de milissegundos desde a época do Unix.

new Date(0) - 0
==> 0 - 0
==> 0

O operador + acionará a conversão padrão. Date assumirá uma conversão para string, portanto o método toString() será utilizado, ao invés do valueOf().

new Date(0) + 0
==> 'Thu Jan 01 1970 02:00:00 GMT+0200 (EET)' + 0
==> 'Thu Jan 01 1970 02:00:00 GMT+0200 (EET)0'

Rápidas explicações

O que é um operador unário e binário?

  • Unário: aquele que interage sobre um elemento. Ex: +, -, ++.
  • Binário: aquele que interage sobre dois elementos. Ex: +, -, *, /, &, &&.

Referências

Recomendo o excelente livro“Understanding ES6” escrito por Nicholas C. Zakas. É uma grande fonte para aprender ES6, não é tão avançado, e não fica muito tempo em partes mais profundas.

E aqui um ótimo livro de ES5 — SpeakingJS written por Axel Rauschmayer.

(Russian) Современный учебник Javascript — https://learn.javascript.ru/. Especially these two pages on type coercion.

JavaScript Comparison Table — https://dorey.github.io/JavaScript-Equality-Table/

wtfjs — a little code blog about that language we love despite giving us so much to hate — https://wtfjs.com/

--

--