Types in JavaScript

June 3, 2025

Types are a fundamental JavaScript concept.

Here are some questions to warm you up!

1/ 8

console.log(Boolean(Boolean(false)))

True
False

You might encounter questions like this in a typical JavaScript Interview.

Let's understand the concepts at play here, so that you can answers these questions with confidence in your next JS interview.

Primitive Types

You might have heard that Everything in JS is an object, Well this is not true.
The reason people say it is because, Almost everything in JS can behave like an object (thanks to boxing), but that does not make them object themselves.


We have 7 primitive types in JS:

  • string : e.g., 'hello', 'world'

  • number: e.g., 7, 3.14, NaN

  • bigint: For very large numbers e.g., 123n

  • boolean: true or false

  • undefined: a variable that has been declared but not assigned a value

  • symbol: unique and immutable value, often used as object keys

  • object: e.g., {}, [], () => {}

    Arrays and Functions are also of the object type.

typeof

To check the type of a variable use the typeof keyword.

Unlike languages like C++ (statically typed), in the dynamically typed languages like JS it's not the variables that have types, but the values do.
Therefore when you console typeof <variable-name>, it tells the type of the value in the variable.

let a
console.log(typeof a) // "undefined" ==> Default value for declared but uninitialized
a = "1"
console.log(typeof a) // "string"
a = 2
console.log(typeof a) // "number"
a = true
console.log(typeof a) // "boolean"
a = {}
console.log(typeof a) // "object"
a = Symbol()
console.log(typeof a) // "symbol"

typeof always returns a string, from a set enum of values like (undefined, string, number, ...). So always use string when checking for types.

if (typeof a === "undefined") {
console.log("Yes")
} else console.log("No")
// Yes
if (typeof a === undefined) {
console.log("Yes")
} else console.log("No")
// No

OK, so the cases till now were straight forward, let's get to some weird part of typeof

a = {}
console.log(typeof a) // "object"
a = function () {}
console.log(typeof a) // "function"
a = [1, 2, 3]
console.log(typeof a) // object

So we know that objects, functions and array are all of type object.

Then why do typeof returns function as a type for a function?

All functions in JavaScript are objects.

But the typeof operator was designed to return 'function' for functions, even though they are technically of type object, to make it easier for developers to identify functions.

You might then argue that why then typeof returns object for an array and not array.

Arrays are objects, and JavaScript’s typeof operator only returns 'object' for all non-function objects, including arrays.

typeof was designed in the early days of JavaScript, and it's a bit inconsistent.

The 'function' type was added later as a special case but typeof never got similar special handling for arrays.

Why not change typeof [] == "array" now ?

Again doing it now would break a lot of things, that expect the type of an array to return object.

So how am I supposed to know if a variable is an array or an object, if typeof returns object in both the cases.

We use Array.isArray in that case.

a = {}
console.log(Array.isArray(a)) // false
a = [1, 2, 3]
console.log(typeof a) // object
console.log(Array.isArray(a)) // true

Special Values

NaN

NaN is a special value of type number, that signifies invalid number.

var age = Number("0o31") // 25
var nextYearAge = Number(26) // 26
var newAge = Number("twenty-five") // NaN

Let's consider this example:

let x = Number("test") // NaN
let y = Number("test") // NaN
console.log(x === y) // false, Why??

To check if a value would convert into NaN on number conversion use the isNaN function.

isNaN(NaN) // true
isNaN("test") // true
isNaN("NaN") // true

Notice that even the string values like 'test' and 'NaN' that convert to NaN on number conversion are returning true when checking with isNaN function.

If you want to explicitly check for just the NaN keyword use Number.isNaN function

Number.isNaN("NaN") // false
Number.isNaN("test") // false
Number.isNaN(NaN) // true

Negative Zero

Just like we have negative numbers, JS also has negative 0. Think of it like 0 is the magnitude and - represents the direction. So negative zero can be used to represent an object that has stopped moving backwards.

Let's see how it behaves:

let x = -0
x === -0 // true
x === 0 // true
x.toString() // '0'
x < 0 // false
x > 0 // false

As you saw on using toString() on x we get 0. Hence -0 === 0 because of string conversion that happened.

To check if a number is -0 use object.is()

Object.is(-0, x) // true
Object.is(0, x) // false

Fundamental types

In addition to the primitive types we also have fundamental objects, (aka : Built-in Objects or Native functions)

The fundamental objects, are not really types but they provide a class based form for the primitive types. We use the new keyword to create a constructor of these types. More Java like behavior!

// Use new keyword with
new Object()
new Array()
new Function()
new Date()
new RegExp()
new Error()
// Don't use the new keyword with
String()
Number()
Boolean()

Strings, Number and Boolean should be used as functions and not constructors. Since when used as a function string, number and boolean coerce any value to that respective primitive type. We will discuss that in the Boolean section

Coercion

Coercion is just a fancy name for conversion in JS.

Let's look at how string, number and boolean handle type conversions.

If we are using non-primitive types in an expression. Then JS will recursively use the primitive functions (String, Number, Boolean) to convert the types until we get a primitive value or an error.

Now the order in which these functions get called depends on the type of operation. We will talk about that a bit later, first let's see what happens to different variables on using these functions.

String

String(null) // 'null'
String(undefined) // 'undefined'
String(true) // 'true'
String(false) // 'false'
String(3.14159) // '3.14159'
String(0) // '0'
String(-0) // '0'

On Array:

String([]) // ''
String([1,2,3]) // '1,2,3'
String([null]) // ''
String([undefined]) // ''
String([null, undefined]) // ','
String([], [], [], []) // ''
String([,,,]) // ',,'

On Object:

String({}) // '[object Object]'
String({a: 2}) // '[object Object]'

For any object String() would return '[object Object]'

Number

Number("") // 0
Number("0") // 0
Number("-0") // -0
Number(" 009 ") // 9
Number("3.14159") // 3.14159
Number("0.") // 0
Number(".0") // 0
Number(".") // NaN
Number("0xaf") // 175
Number(false) // 0
Number(true) // 1
Number(null) // 0
Number(undefined) // NaN

On Array & Object:

Calling Number() on arrays and objects, returns the same object, so String() is called next. And then the process continues till we get a primitive type or an error.

Let's understand this with an interactive example:

Number([""])

Let's start with an empty array, and see how it converts to a number.



Let's see another example:

Number([null])

Let's start with an array with a null value, and see how it converts to a number.



One more?

Number({})

This time let's try to convert an object to a number.

Boolean

For boolean it's a simple table look up, no conversion.

  1. false

  2. 0 / -0

  3. NaN

  4. "" (empty string)

  5. null

  6. undefined

Apart from these six values everything else is truthy!

Consider the following example:

Boolean(Boolean(false)) // false
// But
Boolean(new Boolean(false)) // true, Why??

Since, new Boolean is a object wrapper for the primitive boolean type, hence it returns an object. And object is not in the 6 falsy values, we get true.

That's why you should not use the new keyword for String(). Number() & Boolean().

// In fact
Boolean(new Number()) // true
Boolean(new String()) // true
Boolean(new Date()) // true
// and so on

Order of operation

Now that we know how things convert using String, Number and Boolean. Let's see the order:

Addition

  • Both operands are first coerced to primitives (via ToPrimitive).
  • If either resulting primitive is a string, JavaScript performs string concatenation—it converts the other operand to a string (via ToString) and joins them.
  • Otherwise, it converts both operands to numbers (via ToNumber) and performs numeric addition.

Let's run through some examples:

"1" + 123

Since both the operands are of primitive types, ToPrimitive() is not called.


1 + true

Since both the operands are of primitive types, ToPrimitive() is not called.

Subtraction, Multiplication, Division, Remainder, Exponentiation

  • These always convert both operands to numbers (using ToNumber) and then perform the numeric operation.
  • If either conversion yields NaN, the result is NaN
"6" - 2 // 4 (ToNumber("6") - 2)
"6" * "3" // 18 (6 * 3)
"foo" - 10 // NaN (ToNumber("foo") is NaN)

Relational Operators

JavaScript’s relational operators (<, >, <=, >=) use the Abstract Relational Comparison algorithm, which basically works like this:

  1. Evaluate operands left-to-right

    • They’re not all done “simultaneously” – a < b < c is interpreted as (a < b) < c.
  2. ToPrimitive → ToNumber or string comparison

    • Both operands are first coerced to primitives (via ToPrimitive).
    • If after that both are strings, they’re compared lexicographically (dictionary order, character by character, using Unicode code points).
    • Otherwise, both are converted to numbers (ToNumber) and compared numerically.

All this might sound a bit complicated, but it’s actually quite straightforward once you see it in action.

Consider the following examples:

console.log(1 < 2 < 3) // true
console.log(3 > 2 > 1) // false, why?

Let's break it down step by step.

1 < 2 < 3

Let's start with the first example 1 < 2 < 3

Now to the interesting example:

3 > 2 > 1

Consider 3 > 2 > 1

Let's do one more example:

"200" < "30"

Since both the operands are strings, we compare them lexicographically (dictionary order, character by character, using Unicode code points).

== Vs ===

console.log(4 == "4") // true
console.log(4 === "4") // false

The first line for the == in the spec says that if the types of the 2 operand are same use the === Otherwise using == would try to convert both the values to same type and return the result.

So the real difference between == and === is that == allows coercion to happen but === does not. The == prefers Numeric comparisons, so if either of x or y is a number it tries to convert the other one to Number as well

Also in case of ==, if there is a non-primitive the toPrimitive() is called and once it becomes a primitive the comparison is made

console.log([42] == 42) // true
// toPrimitive([42]) = "42"
// Number("42") = 42
// 42 == 42 // true

Let's now look at the example from the quiz:

We saw that [] == ![] is true.

Let's see why?

[] == ![]

Let's start with the expression [] == ![]

Now this is not how you should do Boolean comparisons.

The correct expression should be [] != []

Now even this is true, but the reason is different.

Here we are comparing two different arrays, and since they are not the same object, they are not equal.

Exercise

Now with all that knowledge, see if you can tell the output of the following code:

const a = []
if (a) {
console.log("yes 1")
}
if (a == true) {
console.log("yes 2")
}
if (!a == !true) {
console.log("yes 3")
}
if (a == false) {
console.log("yes 4")
}

Now the output may not have made sense.

To understand it clearly, you need to understand the difference between Boolean Context & Equality Comparison.

Let's see each of the if statements one by one:

  • if (a) {
    console.log("yes 1")
    }

    Here a is an array, and arrays are truthy values.
    So this condition is true, and we print "yes 1".
    This was the boolean context. No coercion happened here.


  • if (a == true) {
    console.log("yes 2")
    }

    Here we are comparing an array with a boolean. So we convert the array to a string, which is "", and then convert that to a number, which is 0.

    Then we compare 0 with true, which is 1.

    So this condition is false, and we don't print anything.
    This is called Equality Comparison.


  • if (!a == !true) {
    console.log("yes 3")
    }

    Here we are comparing the negation of an array with the negation of true. The negation of an array is false, and the negation of true is also false.

    So this condition is true, and we print "yes 3". This is also a Boolean Context.


  • if (a == false) {
    console.log("yes 4")
    }

    Here we are comparing an array with false.So we convert the array to a string, which is "", and then convert that to a number, which is 0.

    Then we compare 0 with false, which is also 0.
    So this condition is true, and we print "yes 4". This is also an Equality Comparison.

Therefore is recommended to make sure that the ! (not) sign is with the equal to symbol, and not with the value, otherwise it would first do a Boolean table lookup and produce weird results.

The Special Case of null

JSON.stringify(), converts undefined to null.

null is only equal to undefined or itself using the == operator.

JSON.stringify([1,2,null,3]) // '[1,2,null,3]'
JSON.stringify([1,2,undefined,3]) // '[1,2,null,3]'
null === undefined // false, since the types are different
null == undefined // true
null == 0 // false (Here null is converted to +0)
null > 0 // false
null < 0 // false
// But
null <= 0 // true
null >=0 // true


console.log(undefined == 0)
console.log(undefined < 0)
console.log(undefined > 0)
console.log(undefined <= 0)
console.log(undefined >= 0)
// All are false, since undefined is converted to NaN

Also,

null == false // false
null == true // false

If we add + in front of null the coercion happens, and +null becomes 0. Hence:

+null == false // true
+null == true // false

Similarly -null becomes -0

Wrapping Up

Understanding JavaScript's type system is crucial for writing robust code and succeeding in technical interviews. Let's recap the key insights from the blog.

The Core Principles

Types belong to values, not variables. Unlike statically typed languages, JavaScript's dynamic nature means a single variable can hold different types throughout its lifetime. This fundamental concept underlies everything else we've explored.

Coercion is predictable, not magical. While JavaScript's type conversions might seem mysterious at first, they follow consistent rules. The String(), Number(), and Boolean() functions are your guide to understanding how any value will be converted.

Critical Distinctions to Remember

The difference between boolean context and equality comparison is crucial to understand. A value might be truthy in an if statement but still equal false when compared directly. This distinction trips up many developers and is a favorite topic in interviews.

Similarly, == vs === isn't just about "loose" vs "strict" - it's about whether you want coercion to happen. Both have their place, but understanding when coercion occurs helps you choose the right tool.

Practical Takeaways

  • Use Array.isArray() instead of relying on typeof for arrays
  • Remember the six falsy values - everything else is truthy
  • Be cautious with implicit coercion in operations, especially with mixed types
  • When in doubt, be explicit with your type conversions

Final Thoughts

These concepts might seem like JavaScript quirks, but they reflect the language's flexibility and its origins as a quickly-developed scripting language that has evolved into a powerful programming platform.

Understanding these behaviors helps you write more predictable code and debug issues more effectively.

With this foundation, you're now equipped to handle even the trickiest type-related questions in interviews and write more confident JavaScript in your projects. The key is practice - try to predict the outcome of expressions before running them, and you'll internalize these patterns quickly.

Where to go from here?

I highly recommend the You Don't Know JS series by Kyle Simpson. It dives deep into JavaScript's mechanics, including types and coercion, and is a fantastic resource for mastering the language.