Type Conversion
Type conversion typically occurs during the compilation phase in statically-typed languages, whereas in dynamically-typed languages, type coercion happens at runtime. This is why type conversion is ubiquitous in daily development and interviews—we often need to manually perform coercion or rely on the language’s implicit conversion mechanisms.
In JavaScript, type conversion is generally referred to as type coercion. It can be further categorized into implicit coercion and explicit coercion, with a clear distinction between the two. Explicit coercion involves developers intentionally invoking conversion functions, while implicit coercion is less obvious and often arises as a side effect of certain operations.
As shown in the following code:
var a = 77;
var b = a + ''; // Implicit coercion
var c = String(a); // Explicit type conversion
toString
toString()
is a method that every object in JavaScript has, used to convert the object into a string. Different types of data will return different results when calling toString()
.
Basic Usage
Plain Objects: By default, it returns "[object Object]"
.
const obj = {};
console.log(obj.toString()); // "[object Object]"
Numbers: Returns the string representation of the number.
const num = 123;
console.log(num.toString()); // "123"
Booleans: Returns “true” or “false”.
const bool = true;
console.log(bool.toString()); // "true"
Arrays: Converts the array elements into a comma-separated string.
const arr = [1, 2, 3];
console.log(arr.toString()); // "1,2,3"
Functions: Returns the source code string of the function.
const func = () => 'hello';
console.log(func.toString()); // () => "hello"
You can override the toString()
method of an object to define your own string representation:
const person = {
name: 'Moment',
toString: function () {
return this.name;
},
};
console.log(person.toString()); // "Moment"
When performing string concatenation, JavaScript will automatically call toString()
:
const obj = { name: 'Moment' };
console.log('Hello, ' + obj); // "Hello, [object Object]"
The toString()
method helps convert objects into strings. Different types of data and objects have different conversion methods, and you can customize an object’s string representation by overriding toString()
.
valueOf()
The valueOf()
method is used to convert an object into a primitive value, and it is typically prioritized when performing numeric operations. The toString()
method, on the other hand, is used to convert an object into a string and is prioritized during string operations. If toString()
returns a string type, valueOf()
is generally not invoked because the string conversion is already accomplished by toString()
.
As shown in the code below:
const obj = {
valueOf: () => 42,
toString: () => 'Hello',
};
// In numeric operations, valueOf() is called first
console.log(10 + obj); // Output: 52, because obj.valueOf() is called, returning 42
// During string concatenation, valueOf() is prioritized and its result is converted to a string
console.log('Message: ' + obj); // Output: Message: 42, because obj.valueOf() is called, returning 42
// When explicitly converting to a string, toString() is invoked
console.log(String(obj)); // Output: Hello, calls obj.toString()
Through this example, we can observe the priority order of valueOf()
and toString()
during different types of type conversions.
Symbol.toPrimitive
Symbol.toPrimitive
is a built-in Symbol property in JavaScript that defines how an object is converted to a primitive value. When an object needs to undergo type coercion, the method specified by this property is prioritized and invoked first.
This method accepts a hint
parameter indicating the expected primitive type for conversion. The possible values for hint
are:
"number"
: Convert to a numeric type."string"
: Convert to a string type."default"
: A generic type, typically determined automatically by JavaScript.
This method supports all type coercion algorithms (such as numeric operations, string concatenation, etc.). In these scenarios, JavaScript prioritizes calling Symbol.toPrimitive
to ensure the object is correctly converted to the expected primitive value.
The example below demonstrates how the Symbol.toPrimitive
property modifies the primitive value derived from an object:
const object = {
[Symbol.toPrimitive](type) {
if (type === 'number') return 77;
if (type === 'string') return 'string priority here';
// When handling the "default" type, return the desired value directly
if (type === 'default') return 'default';
},
valueOf() {
return 22;
},
toString() {
return 33;
},
};
console.log(String(object)); // "string priority here", type parameter is "string"
console.log(Number(object)); // 77, type parameter is "number"
console.log(object + ''); // "default", type parameter is "default"
JSON Stringification
The utility function JSON.stringify(...)
calls the toString()
method when serializing a JSON object into a string. The behavior of JSON.stringify
and toString()
is largely similar, except that JSON.stringify
always returns a string formatted according to JSON standards, while toString()
returns a string representation of the object, typically a concise description.
console.log(JSON.stringify(77)); // '77'
console.log(JSON.stringify('77')); // '"77"'
console.log(JSON.stringify(null)); // 'null'
console.log(JSON.stringify(undefined)); // undefined
When encountering undefined
, function()
, or Symbol
values in objects, JSON.stringify(...)
automatically ignores them. For arrays, these values are replaced with null
, for example:
console.log(JSON.stringify(undefined)); // undefined
console.log(JSON.stringify(function () {})); // undefined
console.log(JSON.stringify(class C {})); // undefined
console.log(JSON.stringify([1, undefined, function () {}, 4])); // [1,null,null,4]
console.log(JSON.stringify({ a: 2, b() {} })); // "{"a":2}"
console.log(JSON.stringify({ x: undefined, y: Object, z: Symbol('') })); // '{}'
JSON.stringify(...)
throws an error when the object contains circular references. A circular reference occurs when a property of an object directly or indirectly references the object itself. Additionally, JSON.stringify()
may throw errors in the following cases:
-
Unserializable values: Properties with values of type
undefined
,function
, orSymbol
are ignored. If the object contains aBigInt
value,JSON.stringify()
throws aTypeError
becauseBigInt
cannot be serialized into JSON. -
Non-enumerable properties:
JSON.stringify()
only serializes enumerable properties and ignores non-enumerable ones. -
Invalid values returned by
toJSON()
: If thetoJSON()
method of an object returns a value containing unserializable data (e.g.,undefined
orBigInt
), serialization will fail.
Example code:
const obj1 = {
name: 'John',
func: function () {
return 'Hello';
}, // Functions are ignored
};
const obj2 = {
name: 'Jane',
value: Symbol('unique'), // Symbols are ignored
};
const obj3 = {
name: 'Alice',
bigIntValue: BigInt(123), // BigInt causes a TypeError
};
// No error, but func is ignored
console.log(JSON.stringify(obj1)); // Output: {"name":"John"}
// No error, but Symbol is ignored
console.log(JSON.stringify(obj2)); // Output: {"name":"Jane"}
try {
console.log(JSON.stringify(obj3)); // Throws TypeError
} catch (error) {
console.error('Error: BigInt cannot be processed by JSON.stringify()', error);
}
If an object defines a toJSON()
method, JSON.stringify()
prioritizes calling this method and uses its return value for serialization. This allows you to ensure the returned value is safe and JSON-compliant, avoiding errors caused by invalid JSON values.
Example code:
const obj = {
name: 'John',
age: 30,
birthdate: new Date(), // Date objects are not directly serializable
toJSON: function () {
return {
name: this.name,
age: this.age,
birthdate: this.birthdate.toISOString(), // Convert Date to ISO string
};
},
};
const jsonString = JSON.stringify(obj);
console.log(jsonString);
The output is shown below:
Replacer
The second parameter of JSON.stringify()
, replacer, controls which properties of an object should be serialized. It can be either an array or a function, each serving distinct purposes.
Replacer as an Array
If the replacer is an array of strings, only the properties listed in the array will be included in the JSON result. Other properties are ignored.
Example code:
var a = {
foo: 77,
bar: 'moment',
baz: [1, 2, 3],
};
console.log(JSON.stringify(a, ['bar', 'baz']));
// Output: '{"bar":"moment","baz":[1,2,3]}'
// Only "bar" and "baz" are included; "foo" is ignored
This specifies which properties to serialize by explicitly listing them.
Replacer as a Function
If the replacer is a function, it is called for each property to decide whether to serialize it. The function accepts two parameters:
- key: The name of the property being processed.
- value: The value of the property.
If the function returns undefined
, the property is ignored. Otherwise, the returned value is used for serialization.
Example code:
var a = {
foo: 77,
bar: 'moment',
baz: [1, 2, 3],
};
console.log(
JSON.stringify(a, function (key, value) {
if (key === 'foo') return undefined; // Ignore "foo"
return value; // Serialize other properties
}),
);
// Output: '{"bar":"moment","baz":[1,2,3]}'
// "foo" is ignored; "bar" and "baz" are serialized
This allows dynamic control over property serialization using custom logic.
ToNumber
Sometimes we need to treat non-numeric values as numbers, such as in numeric operations. For this purpose, the ES5 specification defines the abstract operation ToNumber
, with the basic syntax ToNumber(argument)
. When invoked, it performs the following steps:
- If
argument
is of the number type, returnargument
directly. - If
argument
is aSymbol
orBigInt
, throw aTypeError
. - If
argument
isundefined
, returnNaN
. - If
argument
isnull
orfalse
, return+0
. - If
argument
istrue
, return1
. - If
argument
is a string type, invoke theStringToNumber(argument)
method. If the string contains non-numeric characters (e.g.,"1,2"
), theNumber
function converts it toNaN
.
If the input is an object (including arrays), it is first converted to a primitive value. If the resulting value is not a number, it is further coerced to a number following Rule 6.
To convert an object to a primitive value, the abstract operation ToPrimitive
invokes the internal method [[DefaultValue]](hint)
:
- If the
hint
parameter is"number"
:- Check if the object has a
valueOf()
method.- If
valueOf()
exists, call it and return the result. - If the result is a primitive value, use it for type coercion.
- If
valueOf()
does not exist or does not return a primitive value, calltoString()
.
- If
- Check if the object has a
- The
toString()
method follows the same rules asvalueOf()
. If it returns a primitive value, type coercion proceeds.
If neither valueOf()
nor toString()
returns a primitive value, a TypeError
is thrown.
Starting in ES5, objects created via Object.create(null)
have a [[Prototype]]
of null
and thus lack valueOf()
and toString()
methods, making them impossible to coerce into primitive values.
var a = {
valueOf: function () {
return '66';
},
toString() {
return '77';
},
};
var b = {
toString() {
return '77';
},
};
var c = [2, 3];
c.toString = function () {
return this.join('');
};
console.log(Number(a)); // 66 (valueOf() is prioritized)
console.log(Number(b)); // 77 (toString() is called)
console.log(Number(c)); // 23 (custom toString())
console.log(Number('')); // 0 (empty string → 0)
console.log(Number([])); // 0 ([] → "" → 0)
console.log(Number([1, 2])); // NaN ("1,2" is not a valid number)
console.log(Number(['a'])); // NaN ("a" is not a number)
console.log(Number('12ab')); // NaN (invalid numeric string)
In the example above:
- Numeric coercion prioritizes
valueOf()
, followed bytoString()
. - For array
c
, thetoString()
method is overridden to return"23"
, which is then coerced to23
. - Arrays are converted by calling
toString()
:[]
becomes""
(coerced to0
).[2,3]
becomes"2,3"
(invalid number →NaN
).- Non-numeric strings (e.g.,
"12ab"
) also result inNaN
.
ToBoolean
In JavaScript, the keywords true
and false
represent the boolean values for truth and falsity. The ECMAScript (ES) specification defines the abstract operation ToBoolean
, which takes an argument argument
and follows these rules:
- If
argument
is aCompletion Record
and is an abrupt completion, returnargument
directly. Otherwise, returnToBoolean(argument.[[value]])
. - If
argument
isundefined
, returnfalse
. - If
argument
isnull
, returnfalse
. - If
argument
is aBoolean
, returnargument
directly. - If
argument
is aNumber
and is+0
,-0
, orNaN
, returnfalse
; otherwise, returntrue
. - If
argument
is aString
and is empty (length0
), returnfalse
; otherwise, returntrue
. - If
argument
is aSymbol
, returntrue
. - If
argument
is anObject
(including[]
,{}
,function(){}
), returntrue
(but see Rule 9). - If
argument
is anObject
with the[[IsHTMLDDA]]
internal slot (e.g.,document.all
), returnfalse
.
console.log(Boolean(undefined)); // false
console.log(Boolean(null)); // false
console.log(Boolean(true)); // true
console.log(Boolean(+0)); // false
console.log(Boolean(-0)); // false
console.log(Boolean(NaN)); // false
console.log(Boolean(777)); // true
console.log(Boolean('')); // false
console.log(Boolean('1')); // true
console.log(Boolean('""')); // true (non-empty string)
console.log(Boolean(Symbol())); // true
console.log(Boolean(function () {})); // true
console.log(Boolean({})); // true
console.log(Boolean([])); // true
console.log(Boolean(document.all)); // false (exception case)
Completion Record
A Completion Record
is a special type of Record
used to describe the outcome of executing a specific step in a process. For example, when executing var a = 2;
in the console, the statement completes successfully but returns undefined
. Ordinary statements produce a normal
Completion Record (indicating execution should proceed to the next step), while expression statements return a Completion Record with a [[value]]
field representing the computed result. For var
declarations, the Completion Record’s [[value]]
is empty.
In the ES specification, every step or statement execution explicitly or implicitly returns a Completion Record with three fields:
-
[[type]]
: Indicates the type of Completion Record. Possible values:normal
: Execution completed normally.return
: Triggered by areturn
statement.throw
: Triggered by a thrown exception.break
/continue
: Control flow jumps (likegoto
).
-
[[value]]
:- For
normal
,return
, orthrow
types: Holds the resulting value or exception. - For
break
/continue
:empty
.
- For
-
[[target]]
:- For
break
/continue
: Specifies the targetlabel
for control flow.
- For
A Completion Record with [[type]]
as normal
is called a normal completion; other types are abrupt completions.
Explicit Type Coercion
Explicit type coercion refers to intentional and obvious type conversions. Many type conversions fall into this category.
Explicit Conversion Between Strings and Numbers
Conversions between strings and numbers are achieved using the built-in functions String(...)
and Number(...)
, which do not use the new
keyword and thus do not create wrapper objects. For example:
var a = 77;
console.log(String(a)); // "77"
var b = '3.14';
console.log(Number(b)); // 3.14
In addition to String(...)
and Number(...)
, other methods can perform explicit conversions between strings and numbers:
var a = 77;
console.log(a.toString()); // "77"
var b = '3.14';
console.log(+b); // 3.14
While a.toString()
is explicit, it involves implicit conversion. Since primitives like 77
do not have a toString()
method, JavaScript automatically creates a wrapper object (e.g., new String(a)
) and calls toString()
on it. This explicit conversion includes an implicit step. The code a.toString()
effectively executes:
var a = 77;
var aa = new String(a); // Implicit wrapper object creation
console.log(aa.toString()); // "77"
In the example +b
, the unary +
operator (with a single operand) explicitly converts b
to a number, rather than performing addition or string concatenation.
Other examples of unary operators:
var a = '3.14';
var b = 3.14 + +a; // The second `+` converts "3.14" to 3.14
console.log(b); // 6.28
console.log(1 + -+(+(+-+1))); // 2 (demonstrating nested unary operators)
Let’s examine more cases:
console.log(+[]); // 0 ([] → "" → 0)
console.log(+['1']); // 1 (["1"] → "1" → 1)
console.log(+['1', '2', '3']); // NaN ("1,2,3" is invalid)
console.log(+{}); // NaN ({} → "[object Object]" → NaN)
For +[]
, JavaScript first attempts valueOf()
, but since arrays lack a meaningful valueOf()
, it calls toString()
, which returns ""
. Converting ""
to a number yields 0
. Similarly, ["1"]
becomes "1"
, which converts to 1
, while ["1", "2"]
becomes "1,2"
, resulting in NaN
. Objects like {}
return "[object Object]"
, which converts to NaN
.
Explicit Parsing of Numeric Strings
Parsing a numeric string (e.g., parseInt
) and converting it to a number (e.g., Number
) both produce numbers, but they behave differently:
var a = '77';
var b = '77px';
console.log(Number(a)); // 77
console.log(parseInt(a)); // 77
console.log(Number(b)); // NaN (non-numeric characters)
console.log(parseInt(b)); // 77 (parses until non-numeric character)
parseInt
parses left-to-right and stops at the first invalid character, while Number
requires the entire string to be numeric.
A tricky example:
console.log(parseInt(1 / 0, 19)); // 18
Explanation: 1 / 0
evaluates to Infinity
. parseInt("Infinity", 19)
parses the first character "I"
, which represents 18
in base 19 (digits 0-9 and letters a-i). The second character "n"
is invalid in base 19, so parsing stops at "I"
, returning 18
.
Other examples of parseInt
quirks:
console.log(parseInt(0.000008)); // 0 ("0.000008" parsed as 0)
console.log(parseInt(0.0000008)); // 8 ("8e-7" parsed as 8)
console.log(parseInt(false, 16)); // 250 ("fa" from "false")
console.log(parseInt(parseInt, 16)); // 15 ("f" from "function")
console.log(parseInt('0x10')); // 16 (hexadecimal notation)
console.log(parseInt('103', 2)); // 2 (binary, "10" = 2, "3" invalid)
When parseInt
receives an object, it implicitly calls the object’s toString()
method and parses the result:
var obj = {
a: 1,
toString() {
return this.a;
}, // Used by parseInt
valueOf() {
return 2;
},
toJSON() {
return 3;
},
};
console.log(parseInt(obj)); // 1 (from toString())
Explicit Conversion to Boolean
The Boolean(...)
function performs explicit ToBoolean
coercion. While explicit, it is rarely used directly. Instead, the double negation !!
is a common pattern to coerce a value to a boolean:
var a = '0';
var b = [];
var c = {};
var d = '';
var e = 0;
var f = null;
var g;
console.log(!!a); // true (non-empty string)
console.log(!!b); // true (object)
console.log(!!c); // true (object)
console.log(!!d); // false (empty string)
console.log(!!e); // false (0)
console.log(!!f); // false (null)
console.log(!!g); // false (undefined)
The first !
converts the value to a boolean and inverts it, while the second !
inverts it back to the original boolean equivalent.
Implicit Type Coercion
Implicit type coercion refers to hidden type conversions with non-obvious side effects. In other words, any coercion that is not explicitly clear to the developer can be considered implicit. While explicit coercion aims to improve code clarity, implicit coercion often makes code harder to understand.
Implicit Coercion Between Strings and Numbers
The +
operator is overloaded to handle both numeric addition and string concatenation. How does JavaScript determine which operation to perform? For example:
var a = '77';
var b = '0';
var c = 77;
var d = 0;
console.log(a + b); // "770" (string concatenation)
console.log(c + d); // 77 (numeric addition)
Why do we get different results? A common assumption is that +
performs string concatenation if either operand is a string. However, the reality is more nuanced. Consider:
var a = [1, 2];
var b = [3, 4];
console.log(a + b); // "1,23,4" (both arrays coerced to strings)
Neither a
nor b
are strings, but they are implicitly coerced to strings and concatenated. Why?
Based on the ES5 specification, the rules for +
are as follows:
- Evaluate the left operand (
AdditiveExpression
) and assign it tolref
. - Get the value of
lref
usingGetValue(lref)
, assigned tolval
. - Check if
lval
is an abrupt completion (e.g., error). If so, return it. - Repeat steps 1–3 for the right operand (
rval
). - Convert
lval
to a primitive (lprim
) usingToPrimitive(lval)
. - Convert
rval
to a primitive (rprim
) usingToPrimitive(rval)
. - If either
lprim
orrprim
is a string, coerce both to strings and concatenate. - Otherwise, convert both to numbers (
lnum
andrnum
) and perform numeric addition.
Additional rules for numeric addition:
NaN + any
→NaN
Infinity + Infinity
→Infinity
-Infinity + Infinity
→NaN
Infinity + finite
→Infinity
-0 + 0
→0
-0 + -0
→-0
0 + non-zero
→ non-zero value
Examples:
console.log(null + 1); // 1 (Number(null) → 0)
console.log(undefined + 1); // NaN (Number(undefined) → NaN)
console.log(NaN + 1); // NaN
console.log(-Infinity + Infinity); // NaN
console.log(Infinity + Infinity); // Infinity
console.log(-0 + 0); // 0
In the array example, since arrays lack valueOf()
, toString()
is called, converting [1,2]
to "1,2"
and [3,4]
to "3,4"
, resulting in "1,23,4"
.
Object Coercion in +
Operations
Objects are coerced to primitives using ToPrimitive
, which prioritizes valueOf()
over toString()
:
var obj = {
valueOf() {
return 7;
},
toString() {
return '7';
},
};
var bar = {
toString() {
return '7';
},
};
console.log(obj + obj); // 14 (valueOf() used)
console.log([1] + obj); // "17" ([1] → "1", obj → 7 via valueOf())
console.log(obj + bar); // "77" (obj → 7, bar → "7")
Edge Cases with {}
and []
console.log([] + {}); // "[object Object]" ([] → "", {} → "[object Object]")
console.log({} + []); // 0 ({} parsed as empty block; +[] → 0)
The second case ({} + []
) behaves differently because {}
is interpreted as an empty code block in some contexts, leaving +[]
(which coerces to 0
). Wrapping in parentheses forces object interpretation:
console.log({} + []); // "[object Object]"
The -
Operator
Unlike +
, the -
operator always coerces operands to numbers:
console.log([1, 3] - 2); // NaN ("1,3" → NaN)
console.log(null - 1); // -1 (null → 0)
console.log(undefined - 1); // NaN (undefined → NaN)
console.log(77 - '7'); // 70 ("7" → 7)
console.log([3] - [1]); // 2 ([3] → 3, [1] → 1)
console.log(new Date() - 1); // Timestamp - 1 (Date → number via valueOf())
Key Takeaways
+
prioritizes string concatenation if either operand is a string afterToPrimitive
.- Objects without
valueOf()
fall back totoString()
. -
always performs numeric coercion, ignoring string/object semantics.- Edge cases like
{} + []
depend on parsing context (code block vs. expression).
References
Summary
JavaScript’s type conversion mechanisms include implicit and explicit conversions.
- Implicit conversion is automatically performed by JavaScript, often triggered by operators. For example, the
+
operator may perform string concatenation or numeric addition based on operand types. - Explicit conversion involves manual coercion using built-in functions like
String()
,Number()
, etc. - The
toString()
andvalueOf()
methods are key for object-to-primitive conversion, whileSymbol.toPrimitive
allows customizing conversion logic.
Overall, JavaScript’s type coercion is flexible yet complex, significantly impacting code behavior and outcomes.