Weather forecast, where the host points to clouds with different types

Type Conversion

Pants turn into elegant shorts, but what does the string '55ab' turn into when converted to a number?

Time to read: 11 min

Briefly

Imagine a situation: we have a form with a field where the user enters their age in years.

By default, any input in fields is a string. If we want to work with this value as a number, we need to cast it to a number.

Casting (or type conversion) is the process of converting a value from one type to another.

In JavaScript, types can be converted explicitly and implicitly.

When we call a function to get a specific type — this is explicit conversion:

        
          
          const x = '4'Number(x)const y = 4String(y)
          const x = '4'
Number(x)

const y = 4
String(y)

        
        
          
        
      

Comparison can be strict or loose. In strict comparison (===), the interpreter takes into account the types of the compared values.

When we compare values loosely using ==, JavaScript converts types on its own:

        
          
          console.log(5 == '5')// trueconsole.log(5 === '5')// false
          console.log(5 == '5')
// true
console.log(5 === '5')
// false

        
        
          
        
      

To understand why this is so, we first need to figure out what types exist in JS.

First, we will draw a line between primitive types, objects, and others.

Primitive Types

In JavaScript, primitive types are the following:

        
          
          // 1. Undefinedtypeof undefined === 'undefined'// 2. Booleantypeof true === 'boolean'typeof false === 'boolean'// 3. Numbertypeof 42 === 'number'typeof 4.2 === 'number'typeof -42 === 'number'typeof Infinity === 'number'typeof -Infinity === 'number'// 4. Stringtypeof '' === 'string'typeof 'string' === 'string'typeof 'number' === 'string'typeof 'boolean' === 'string'// 5. Symbol, ES6typeof Symbol() === 'symbol'// 6. BigInt, ES6typeof 9007199254740991n === 'bigint'typeof BigInt(9007199254740991) === 'bigint'// 7. Nulltypeof null === 'object'// We'll discuss why it's "object" a bit later.
          // 1. Undefined
typeof undefined === 'undefined'

// 2. Boolean
typeof true === 'boolean'
typeof false === 'boolean'

// 3. Number
typeof 42 === 'number'
typeof 4.2 === 'number'
typeof -42 === 'number'
typeof Infinity === 'number'
typeof -Infinity === 'number'

// 4. String
typeof '' === 'string'
typeof 'string' === 'string'
typeof 'number' === 'string'
typeof 'boolean' === 'string'

// 5. Symbol, ES6
typeof Symbol() === 'symbol'

// 6. BigInt, ES6
typeof 9007199254740991n === 'bigint'
typeof BigInt(9007199254740991) === 'bigint'

// 7. Null
typeof null === 'object'
// We'll discuss why it's "object" a bit later.

        
        
          
        
      

Primitive types are such types whose values can only be overwritten but not changed.

For example, if we create a variable with a value of 42, it cannot be changed. We can only completely overwrite it:

        
          
          let theAnswerToUltimateQuestion = 42theAnswerToUltimateQuestion = 43// The new value has completely overwritten the old one;// the old one is collected by the garbage collector and forgotten.let theAnswers = [42, 43, 44]theAnswers[0] = 142// Now the variable value is [142, 43, 44];// we did not overwrite it completely, but merely changed a part.
          let theAnswerToUltimateQuestion = 42
theAnswerToUltimateQuestion = 43
// The new value has completely overwritten the old one;
// the old one is collected by the garbage collector and forgotten.

let theAnswers = [42, 43, 44]
theAnswers[0] = 142
// Now the variable value is [142, 43, 44];
// we did not overwrite it completely, but merely changed a part.

        
        
          
        
      

This mechanism is related to how variable values are stored in memory. We won't go too deep into this topic, but broadly speaking, primitive types "refer to the same value in memory," while non-primitive types refer to different ones. We explore this issue further in the article “Reference vs Value Storage”.

Because of this, for instance, primitives can be compared by value:

        
          
          const a = 5const b = 5console.log(a == b)// true
          const a = 5
const b = 5
console.log(a == b)
// true

        
        
          
        
      

But non-primitives cannot be:

        
          
          const a = [1, 2, 3]const b = [1, 2, 3]console.log(a == b)// false
          const a = [1, 2, 3]
const b = [1, 2, 3]
console.log(a == b)
// false

        
        
          
        
      

Even though the arrays contain the same numbers, they are not considered "equal" when compared. When JavaScript compares a and b, it is, roughly speaking, "comparing the memory locations these variables refer to." For non-primitives, these locations are different, which is why they are considered unequal.

Objects

Objects in JavaScript are used to store collections of values.

Arrays (Array) in JS are also objects.

As we mentioned, non-primitives are compared by reference, not by value. Objects and arrays are indeed non-primitives.

Objects in JavaScript have their own type — object.

        
          
          const keyValueCollection = { key: 'value' }typeof keyValueCollection === 'object'const listCollection = [1, 2, 3]typeof listCollection === 'object'
          const keyValueCollection = { key: 'value' }
typeof keyValueCollection === 'object'

const listCollection = [1, 2, 3]
typeof listCollection === 'object'

        
        
          
        
      

For null, the typeof operator returns 'object', even though it is also a primitive:

        
          
          console.log(typeof null === 'object')// true
          console.log(typeof null === 'object')
// true

        
        
          
        
      

Functions

Functions in JavaScript also have a type — object, although typeof returns 'function':

        
          
          function simpleFunction() {}console.log(typeof simpleFunction === 'function')// trueconst assignedFunction = function () {}console.log(typeof assignedFunction === 'function')// trueconst arrowFunction = () => {}console.log(typeof arrowFunction === 'function')// trueconsole.log(typeof function () {} === 'function')// true
          function simpleFunction() {}
console.log(typeof simpleFunction === 'function')
// true

const assignedFunction = function () {}
console.log(typeof assignedFunction === 'function')
// true

const arrowFunction = () => {}
console.log(typeof arrowFunction === 'function')
// true

console.log(typeof function () {} === 'function')
// true

        
        
          
        
      

We have described the differences between different types of functions in the article “Functions”.

typeof

The typeof operator returns not directly the "type," but a string. For all primitives except null, this string will be the name of that primitive.

For objects, it first checks if it can be "called". Functions are just such objects, so the operator returns function.

Although typeof does not always return what we might expect, it is convenient to use in certain cases in code, for example, for determining functions.

Type Conversion

Now that we have unraveled the types, let's see how we can convert values of one type into values of another.

In JavaScript, there are only 3 types of conversion: to string, to number, or to boolean.

To convert a value to these types, we can use the functions of the same name:

        
          
          String(42) // Converts to string.Number('42') // Converts to number.Boolean(42) // Converts to boolean.
          String(42) // Converts to string.
Number('42') // Converts to number.
Boolean(42) // Converts to boolean.

        
        
          
        
      

Casting to string, number, and boolean can be performed on any values:

        
          
          // To string:String(123) // '123'String(-12.3) // '-12.3'String(null) // 'null'String(undefined) // 'undefined'String(true) // 'true'String(false) // 'false'String(function () {}) // 'function () {}'String({}) // '[object Object]'String({ key: 42 }) // '[object Object]'String([]) // ''String([1, 2]) // '1,2'
          // To string:
String(123) // '123'
String(-12.3) // '-12.3'
String(null) // 'null'
String(undefined) // 'undefined'
String(true) // 'true'
String(false) // 'false'
String(function () {}) // 'function () {}'
String({}) // '[object Object]'
String({ key: 42 }) // '[object Object]'
String([]) // ''
String([1, 2]) // '1,2'

        
        
          
        
      

You can also attempt to convert any values to a number. If JavaScript cannot convert a certain value to a number, we get NaNa special value representing Not-a-Number.

        
          
          // To number:Number('123') // 123Number('123.4') // 123.4Number('123,4') // NaNNumber('') // 0Number(null) // 0Number(undefined) // NaNNumber(true) // 1Number(false) // 0Number(function () {}) // NaNNumber({}) // NaNNumber([]) // 0Number([1]) // 1Number([1, 2]) // NaN// Note that Number of an empty array is 0,// of an array with one number — that number// and of an array with multiple numbers — NaN.// We will understand why this is later.
          // To number:
Number('123') // 123
Number('123.4') // 123.4
Number('123,4') // NaN
Number('') // 0
Number(null) // 0
Number(undefined) // NaN
Number(true) // 1
Number(false) // 0
Number(function () {}) // NaN
Number({}) // NaN
Number([]) // 0
Number([1]) // 1
Number([1, 2]) // NaN

// Note that Number of an empty array is 0,
// of an array with one number — that number
// and of an array with multiple numbers — NaN.
// We will understand why this is later.

        
        
          
        
      

You can also convert any values to boolean:

        
          
          Boolean('') // falseBoolean('string') // trueBoolean('false') // trueBoolean(0) // falseBoolean(42) // trueBoolean(-42) // trueBoolean(NaN) // falseBoolean(null) // falseBoolean(undefined) // falseBoolean(function () {}) // trueBoolean({}) // trueBoolean({ key: 42 }) // trueBoolean([]) // trueBoolean([1, 2]) // true// Broadly speaking, everything except for an empty string, zero,// NaN, null, and undefined — is true.
          Boolean('') // false
Boolean('string') // true
Boolean('false') // true
Boolean(0) // false
Boolean(42) // true
Boolean(-42) // true
Boolean(NaN) // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(function () {}) // true
Boolean({}) // true
Boolean({ key: 42 }) // true
Boolean([]) // true
Boolean([1, 2]) // true

// Broadly speaking, everything except for an empty string, zero,
// NaN, null, and undefined — is true.

        
        
          
        
      

Implicit Type Conversion

In the previous section, we converted types "manually," using functions. But JavaScript can perform such conversions for us automatically. (This leads to many quirks for which it is not very fond.)

This type of typing, where the type of a value is determined at assignment time and can change along the program, is called dynamic.

Implicit conversion occurs when we force JavaScript to work with values of different types. For instance, if we want to "add" a number and a string:

        
          
          5 + '3' === '53'5 - '3' === 25 + '-3' === '5-3'5 - +3 === 25 + -3 === 2// Because of this, there arose this joke:Array(16).join('wat' - 1) + ' Batman!'// 'NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN Batman!'
          5 + '3' === '53'
5 - '3' === 2
5 + '-3' === '5-3'
5 - +3 === 2
5 + -3 === 2

// Because of this, there arose this joke:
Array(16).join('wat' - 1) + ' Batman!'
// 'NaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaNNaN Batman!'

        
        
          
        
      

The reason lies in how JavaScript tries to "match" these two types together to work with them.

Let's first look at primitives.

  1. The interpreter will convert primitive values to boolean if we use && or ||.
  2. To string, if we use +, when one of the operands is a string.
  3. To number if:
    1. we use comparison operators <, <=, >, >=;
    2. use arithmetic operations -, + (except for point 2), /, *.
    3. we use the unary plus: +'2' === 2;
    4. we use the loose equality operator ==.

But it doesn’t stop at primitives; JavaScript also implicitly converts non-primitive values.

The interpreter converts them to boolean if we use && or ||. Objects are always true.

When it comes to numbers and strings, things are a bit more interesting. To determine whether to convert a value to a string or a number, JavaScript checks which of the two methods (valueOf() and toString()) is defined in the current object.

  1. If the object is not a Date, the valueOf() method is called first (not delving too deeply into the details of the specification).
  2. If the returned value is a primitive, it is returned.
  3. If not, the other method is called (if valueOf() did not return a primitive, then toString() is called, and vice versa).
  4. If a primitive is returned afterward, it is returned.
  5. If even after this a primitive does not return, there will be an error Uncaught TypeError: Cannot convert object to primitive value.

Examples

        
          
          // 1. Simple objectconst obj1 = {}obj1.valueOf() // {}obj1.toString() // '[object Object]'// To "add" a number with an object,// the first call will be obj1.valueOf().// It will return the object (non-primitive),// after which obj1.toString() will be called.1 + obj1// 1 + '[object Object]'// '1' + '[object Object]'// '1[object Object]'// 2. Object with a specified .valueOf()const obj2 = {}obj2.valueOf = () => 'obj2'obj2.valueOf() // 'obj2'obj2.toString() // '[object Object]'// Now when we declared the .valueOf() method,// it will return a string when called.// Since a string is a primitive,// it will be used in "addition."1 + obj2// 1 + 'obj2'// '1' + 'obj2'// '1obj2'// 2.1. If we were to return a numberconst obj2 = {}obj2.valueOf = () => 42obj2.valueOf() // 42obj2.toString() // '[object Object]'1 + obj2// 1 + 42// 43// 3. Datesconst date = new Date()date.valueOf() // 1467864738527date.toString() // 'Sun Sep 15 2019...'// For dates, the order of methods is reversed:// which means .toString() will be called first,// and only after that — .valueOf().1 + date// 1 + 'Sun Sep 15 2019...'// '1' + 'Sun Sep 15 2019...'// '1Sun Sep 15 2019...'
          // 1. Simple object
const obj1 = {}
obj1.valueOf() // {}
obj1.toString() // '[object Object]'

// To "add" a number with an object,
// the first call will be obj1.valueOf().
// It will return the object (non-primitive),
// after which obj1.toString() will be called.

1 + obj1
// 1 + '[object Object]'
// '1' + '[object Object]'
// '1[object Object]'

// 2. Object with a specified .valueOf()
const obj2 = {}
obj2.valueOf = () => 'obj2'
obj2.valueOf() // 'obj2'
obj2.toString() // '[object Object]'

// Now when we declared the .valueOf() method,
// it will return a string when called.
// Since a string is a primitive,
// it will be used in "addition."

1 + obj2
// 1 + 'obj2'
// '1' + 'obj2'
// '1obj2'

// 2.1. If we were to return a number
const obj2 = {}
obj2.valueOf = () => 42
obj2.valueOf() // 42
obj2.toString() // '[object Object]'

1 + obj2
// 1 + 42
// 43

// 3. Dates
const date = new Date()
date.valueOf() // 1467864738527
date.toString() // 'Sun Sep 15 2019...'

// For dates, the order of methods is reversed:
// which means .toString() will be called first,
// and only after that — .valueOf().

1 + date
// 1 + 'Sun Sep 15 2019...'
// '1' + 'Sun Sep 15 2019...'
// '1Sun Sep 15 2019...'

        
        
          
        
      

Strict and Loose Equality

Implicit conversion is also used when we compare values using loose equality ==.

In contrast to strict equality (===), in it the interpreter tries to convert types to one another for comparison.

The complete algorithm is complex. For convenience, it has been condensed into a large matrix, showing "what is equal to what" under strict and loose equality.

Here is the loose equality table (in green are marked values that are "equal"):

Loose Equality Table

And here is the one for strict:

Strict Equality Table

It is considered good practice to use only strict comparison to avoid implicit type conversion when comparing.

In practice

Advice 1

Always use strict equality when comparing values.

🛠 For convenience, the existence check of an object can be performed with if (object) because objects are always coerced to true.

        
          
          const exists = {}if (exists) {  /* this branch will be executed */}const doesntExist = undefinedif (doesntExist) {  /* this branch will not be executed */}
          const exists = {}
if (exists) {
  /* this branch will be executed */
}

const doesntExist = undefined
if (doesntExist) {
  /* this branch will not be executed */
}

        
        
          
        
      

🛠 If you want to describe a complex structure that can "behave" like a number or string, you can describe the methods .valueOf() or .toString().

        
          
          const ticketPrice = {  amount: 20,  currency: 'USD',  valueOf: () => 20,  toString: () => '$20',}1 + ticketPrice // 1 + 20 -> 21console.log(ticketPrice)// $20
          const ticketPrice = {
  amount: 20,
  currency: 'USD',
  valueOf: () => 20,
  toString: () => '$20',
}

1 + ticketPrice // 1 + 20 -> 21
console.log(ticketPrice)
// $20