Artigo original: JavaScript type coercion explained

Escrito por: Alexey Samoshkin

[Editado em 2/5/2018]: esta publicação, agora, também está disponível em russo. Agradeço a Serj Bulavyk pelos seus esforços.

Conheça as engines

A coerção de tipo é o processo de converter o valor de um tipo em outro (como uma string em um número, um objeto para um valor booleano etc.). Qualquer tipo, seja primitivo ou um objeto, é sujeito a coerção de tipo. Para relembrar, os tipos primitivos são: number, string, boolean, null, undefined e symbol (este último adicionado na ES6).

Como um exemplo de coerção de tipo na prática, observe a Tabela de Comparação do JavaScript, que mostra como o operador de igualdade == se comporta para diferentes tipos, a e b. Essa matriz parece intimidadora devido à coerção de tipo implícita que o operador == realiza, sendo quase impossível lembrar de todas essas combinações. Você, no entanto, não precisa fazer isso. Basta aprender os princípios fundamentais da coerção de tipo.

Este artigo detalha o modo como a coerção de tipo funciona em JavaScript. Ele vai preparar você com o conhecimento essencial para que você se sinta confiante em explicar o que cada uma das expressões a seguir resulta. No final deste artigo, mostrarei as repostas 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 cheia de coisas bobas que você pode fazer como desenvolvedor. Em 90% dos casos de uso, é melhor evitar a coerção de tipo implícita. Considere essa lista como um exercício de aprendizado para testar seu conhecimento sobre o funcionamento da coerção de tipo. Se você estiver entediado, pode encontrar mais exemplos em wtfjs.com.

Falando nisso, às vezes, você pode se deparar com questões desse tipo em uma entrevista para um emprego como desenvolvedor JavaScript. Então, continue lendo.

Coerção implícita x explícita

A coerção de tipo pode ser explícita ou implícita.

Quando um desenvolvedor expressa a intenção de converter entre tipos escrevendo o código apropriado, como Number(valor), isso é chamado de coerção de tipo explícita (ou, em inglês, type casting).

Como o JavaScript é uma linguagem fracamente tipada, valores também podem ser convertidos entre tipos diferentes automaticamente, o que é chamado de coerção de tipo implícita. Isso acontece geralmente quando você aplica operadores à valores de diferentes tipos, como 1 == null, 2/'5', null + new Date(), ou pode ser provocado pelo contexto em torno, como ocorre com if (valor) {…}, onde valor é transformado em booleano.

Um operador que não provoca a coerção de tipo implícita é ===, que é chamado de operador de igualdade estrita. O operador de igualdade fraco == por outro lado, realiza tanto a comparação como a coerção de tipo, se necessário.

A coerção de tipo implícita é 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 para aprender é que há apenas três tipos de conversão de tipo em JavaScript:

  • para string
  • para booleano
  • para número

A segunda, a lógica de conversão para tipos primitivos e para objetos, funciona de modos diferentes, mas ambos podem ser convertidos apenas desses três modos.

Vamos começar com os tipos primitivos primeiro.

Conversão em strings

Para converter explicitamente valores para uma string, aplique a função String(). A coerção implícita é provocada pelo operador binário +, quando qualquer operando é uma string:

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

Todos os valores primitivos são convertidos para strings naturalmente como você pode imaginar:

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 ele somente pode ser convertido explicitamente, não implicitamente. Leia mais neste artigo (em inglês) sobre as regras de coerção em Symbol.

String(Symbol('my symbol'))   // 'Symbol(my symbol)'
'' + Symbol('my symbol')      // TypeError

Conversão em booleano

Para explicitamente converter um valor para booleano, aplique a função Boolean(). A conversão implícita acontece em um contexto lógico, ou é causada pelos operadores lógicos ( || && !) .

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

Nota: os operadores lógicos, como  || e &&, realizam a conversão em booleano internamente, mas, na verdade,  retornam o valor dos operandos originais, mesmo se eles não forem booleanos.

// retorna o número 123, em vez de retornar true
// 'hello' e 123 ainda são coagidos à boleano internalmente para calcular a expressão
let x = 'hello' && 123;   // x === 123

Assim, existem apenas 2 resultados possíveis de conversão para booleano: true ou false. Fica mais fácil lembrar a lista de valores falsos.

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

Qualquer valor que não está na lista é convertido para true, incluindo objetos, funções, Array, Date, tipos definidos pelo usuário, entre outros. Símbolos são valores verdadeiros. Objetos vazios e arrays são valores verdadeiros também.

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

Conversão em números

Para uma conversão explícita, basta aplicar a função Number(), da mesma maneira que você fez com Boolean() e String() .

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

  • operadores de comparação (>, <, <=,>=)
  • operadores bitwise ( | & ^ ~)
  • operadores aritméticos (- + * / % ). Observe que o binário + não causa a conversão numérica, quando qualquer operando é uma string.
  • operador + unário
  • operador de igualdade == (incluindo !=). Observe que == não causa conversão numérica quando ambos os operandos são strings.
Number('123')   // conversão explícita
+'123'          // conversão implícita
123 != '456'    // conversão implícita
4 > '5'         // conversão implícita
5/null          // conversão implícita
true | 0        // conversão implícita

Aqui vemos como os valores primitivos são convertidos 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

Quando uma string está sendo convertida para um número, a engine primeiro remove espaços em branco no começo e final, caracteres \n, \t, retornando NaN se a string aparada não representar um número válido. Se a string estiver vazia, ela retorna 0.

null e undefined são tratados de maneira diferente: null se torna 0 e undefined se torna NaN.

Símbolos não podem ser convertidos em números, seja explicitamente, seja implicitamente. Além disso, TypeError é lançado, em vez de converter silenciosamente para NaN, como acontece com undefined. Veja mais sobre as regras de conversão de símbolos na MDN.

Number(Symbol('my symbol'))    // TypeError
+Symbol('123')                 // TypeError

duas regras especiais para se lembrar:

  1. Quando == está sendo aplicado a null ou undefined, a conversão numérica não acontece. null se iguala somente a null ou undefined e nada mais.
null == 0               // false, null não é convertido em 0
null == null            // true
undefined == undefined  // true
null == undefined       // true

2. NaN não se iguala a nada, nem a si mesmo.

if (valor !== valor) { console.log("Nós estamos lidando com NaN aqui") }

Coerção de tipo para objetos

Até agora, vimos a coerção de tipo para valores primitivos. Isso não é muito emocionante.

Quando se trata de objetos e qunado a engine encountra expressões como [1] + [2,3], primeiro é preciso converter um objeto para um valor primitivo, que então é convertido para o tipo final. Ainda são apenas três os tipos de conversão: em números, em strings e em booleanos.

O caso mais simples é a conversão em booleano: qualquer valor não primitivo é sempre coagido a true, não importando se um objeto ou array é vazio ou não.

Objetos são convertidos para números primitivos através do método interno [[ToPrimitive]], que é responsável tanto pela conversão em números como em strings.

Aqui está uma pseudoimplementação do método [[ToPrimitive]]:

[[ToPrimitive]] é passado com um valor de entrada e com uma preferência de tipo de conversão: Number ou String. O preferredType (tipo preferido, em português) é opcional.

As conversões em números e em strings fazem uso de dois métodos do objeto de entrada: valueOf e toString . Ambos os métodos são declarados em Object.prototype e, assim, estão disponíveis para qualquer método derivado, como Date, Array etc.

Em geral, o algoritmo é o seguinte:

  1. Se a entrada já é um valor primitivo, não faça nada e retorne-a.
  2. Chame input.toString() e, se o resultado for de um tipo primitivo, retorne-o.
  3. Chame input.valueOf() e, se o resultado for de um tipo primitivo, retorne-o.
  4. Se nem input.toString() nem input.valueOf() resultam em um tipo primitivo, lance TypeError.

As conversões em números primeiro chamam valueOf (3) indo depois para toString (2). A conversão em strings faz o oposto: toString (2) seguido de valueOf (3).

A maior parte dos tipos integrados não possui valueOf, nem tem valueOf retornando o próprio objeto this, que, então, é ignorado porque não é de um tipo primitivo. Por isso, conversões em números e em strings podem funcionar do mesmo jeito — ambas acabam chamando toString().

Operadores diferentes podem ocasionar conversões em números ou em strings com a ajuda do parâmetro preferredType. Existem, contudo, duas exceções: o operador de igualdade == e o operador binário +, que acarretam modos de conversão padrão (preferredType não é especificado, ou é iguala ao default). Nesse caso, a maior parte dos tipos integrados assume uma conversão em número por padrão, com exceção de Date, que faz a conversão em string.

Um exemplo de comportamento de conversão de Date:

Você pode sobrescrever os métodos padrão toString() e valueOf() para associar a conversão lógica de objeto para tipo primitivo.

Perceba como bj + '' retorna'101' como string. O operador + ocasiona um método de conversão padrão e, como dito antes, Object assume uma conversão em número como padrão, usando, assim, o método valueOf() primeiro, em vez de toString().

Método Symbol.toPrimitive da ES6

Na ES5, você pode associar à lógica de conversão de objeto para tipo primitivo sobrescrevendo os métodos toString evalueOf.

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

Exemplos

Armados com a teoria, agora, vamos voltar aos nossos 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ê pode encontrar explicações para cada um das expressões.

O operador binário + causa a conversão para números para true e false:

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

O operador aritmético de divisão / causa a conversão em número para a string '6' :

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

Abaixo, vemos que o operador + tem associatividade da esquerda para a direita. Assim, a expressão "number" + 15 roda primeiro. Como um dos operandos é uma string, o operador + ocasiona a conversão em string para o número 15. No segundo passo, a expressão "number15" + 3 é avaliada de maneira similar.

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

Aqui, vemos que a expressão 15 + 3 é avaliada primeiro. Não há necessidade nenhuma de coerção, já que ambos os operandos são números. No segundo passo, a expressão 18 + 'number' é avaliada e, como um dos operandos é uma string, isso causa a conversão em string.

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

Neste exemplo, o operador de comparação &gt; ("maior que") ocasiona em conversão para números de [1] e null .

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

Aqui, vemos que o operador unário + tem maior precedência sobre operador binário +. Assim, a expressão +'bar' é avaliada primeiro. O sinal positivo unário acarreta em conversão em números para a string 'bar'. Como a string não representa um número válido, o resultado é NaN. No segundo passo, a expressão 'foo' + NaN é avaliada.

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

Neste exemplo, o operador == acarreta em uma conversão para números, a string 'true' é convertida em NaN e o booleano true é convertido em 1.

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

false == 'false'   
==> 0 == NaN
==> false

Aqui, temos que o operador == geralmente causa a conversão em número, mas esse não é o caso com null. null iguala somente a null ou a undefined e mais nada.

null == ''
==> false

O operador !! converte ambas as strings, 'true' e'false', em booleano, já que elas não são strings vazias. Então, == apenas verifica a igualdade de dois booleanos true sem nenhuma coerção.

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

O operador == ocasiona em uma conversão em número para um array. O método de arrays valueOf() retorna o próprio array, sendo ignorado porque não é um tipo primitivo. O método de array toString() converte ['x'] na string 'x'.

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

O operador + acarreta a conversão em número para []. O método de array valueOf() é ignorado, porque retorna o próprio array, o qual é não primitivo. O método de array toString retorna uma string vazia.

No segundo passo, a expressão '' + null + 1 é avaliada.

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

Os operadores lógicos || e && coagem os operandos para booelano, mas retornam os operandos originais (não booleanos). 0 é falso, enquanto '0' é verdadeiro, porque é uma string não vazia. O objeto vazio {} é também verdadeiro.

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

Nenhuma coerção é necessária, pois ambos os operandos têm o mesmo tipo. Como == verifica a identidade do objeto (e não sua igualdade) e como os dois arrays são instâncias diferentes, o resultado é false.

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

Todos os operandos são valores não primitivos. Assim, + começa pela esquerda, desencadeando a conversão em número. O método valueOf de Object e de Array retorna o próprio objeto, sendo, então, ignorado. toString() é usado como substituto. O truque aqui é que, primeiro, {} não é considerado um objeto literal, mas uma declaração em bloco, sendo ignorado. A avaliação começa com a próxima expressão +[], que é convertida em 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'

Este exemplo abaixo é melhor explicado passo a passo de acordo com a precedência de operador.

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

O operador - acarreta em conversão em números para Date. Date.valueOf() retornando o número de milissegundos desde a epoch do Unix.

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

O operador + acarreta em um conversão padrão. Date assume a conversão em string como padrão. Então, o método toString() é usado em vez de 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'

Recursos adicionais

Eu gostaria de recomendar o excelente livro Understanding ES6, escrito por Nicholas C. Zakas. É um grande recurso de aprendizado sobre ES6, não muito avançado e que não entra muito nos detalhes.

Aqui temos um bom livro apenas sobre a ES5 – SpeakingJS, escrito por Axel Rauschmayer.

(Russo) Современный учебник Javascript (Livro didático moderno do JavaScript, em português) — https://learn.javascript.ru/. Em especial, essas duas páginas sobre conversão de objetos e de tipo.

Tabela de comparação do JavaScript — https://dorey.github.io/JavaScript-Equality-Table/

wtfjs — um pequeno blog de código sobre essa linguagem que nós amamos, embora nos dê tanta raiva — https://wtfjs.com/