JavaScript is a dynamic language with dynamic types. Variables in JavaScript are not directly associated with any particular value type, and any variable can be assigned (and re-assigned) values of all types:

let foo = 42; // foo is now a number
foo = "bar"; // foo is now a string
foo = true; // foo is now a boolean

JavaScript is also a weakly typed language, which means it allows implicit type conversion when an operation involves mismatched types, instead of throwing type errors.

The Javascript data types are Undefined, Null, Boolean, String, Symbol, Number, BigInt, and Object.

All types except Object define immutable values. We refer to values of these types as primitive values.

In JavaScript, primitive values are immutable — once a primitive value is created, it cannot be changed, although the variable that holds it may be reassigned another value. By contrast, objects and arrays are mutable by default — their properties and elements can be changed without reassigning a new value.

const foo = 42; // foo is a number
const result = foo + "1"; // JavaScript coerces foo to a string, so it can be concatenated with the other operand
console.log(result); // 421

Implicit coercions are very convenient, but can create subtle bugs when conversions happen where they are not expected, or where they are expected to happen in the other direction (for example, string to number instead of number to string). For symbols and BigInts, JavaScript has intentionally disallowed certain implicit type conversions.

All primitive types, except null, can be tested by the typeof operator. typeof null returns object, so one has to use === null to test for null.

Null Type

The Null type has exactly one value, called null. The null value represents the intentional absence of any object value.

function getVowels(str) {
    const m = str.match(/[aeiou]/gi);
    if (m === null) {
        return 0;
    }
    return m.length;
}
console.log(getVowels('sky'));
// 0
Undefined Type

The Undefined type has exactly one value, called undefined.

undefined indicates the absence of a value, while null indicates the absence of an object (which could also make up an excuse for typeof null === "object").

There are several situations where the value undefined is assigned:

  • A return statement with no value (e.g. return;) implicitly returns undefined.
  • Accessing a nonexistent object property (e.g. obj.iDontExist) returns undefined.
  • A variable declaration without initialization (e.g. let x;) implicitly initializes the variable to undefined.
  • Many methods, such as Array.prototype.find() and Map.prototype.get(), return undefined when no element is found.
function getNothing() {
    return;
}
console.log(getNothing());
// undefined

let x;
console.log(x);
// undefined

let person = {};
console.log(person.name);
// undefined

null is a keyword, but undefined is a normal identifier that happens to be a global property. undefined should not be redefined or shadowed.

Boolean type

The Boolean type represents a logical entity and is inhabited by two values: true and false.

let x = true;
let y = false;
console.log(x);
// true
console.log(y);
// false
Number type

The Number type is a double-precision 64-bit binary format IEEE 754 value. It is capable of storing positive floating-point numbers between \(2^{-1074}\) (Number.MIN_VALUE) and \(2^{1024}\) (Number.MAX_VALUE) as well as negative floating-point numbers between \(-2^{-1074}\) and \(-2^{1024}\), but it can only safely store integers in the range \(-(2^{53} − 1)\) (Number.MIN_SAFE_INTEGER) to \(2^{53} − 1\) (Number.MAX_SAFE_INTEGER). Outside this range, JavaScript can no longer safely represent integers; they will instead be represented by a double-precision floating point approximation. You can check if a number is within the range of safe integers using Number.isSafeInteger().

Values outside the range ±(\(2^{-1074}\) to \(2^{1024}\)) are automatically converted:

  • Positive values greater than Number.MAX_VALUE are converted to +Infinity.
  • Positive values smaller than Number.MIN_VALUE are converted to +0.
  • Negative values smaller than -Number.MAX_VALUE are converted to -Infinity.
  • Negative values greater than -Number.MIN_VALUE are converted to -0.
// With decimals:
let x1 = 34.00;

// Without decimals:
let x2 = 34; 

NaN (Not a Number) is a special kind of number value that's typically encountered when the result of an arithmetic operation cannot be expressed as a number.

BigInt Type

The BigInt type represents an integer value. The value may be any size and is not limited to a particular bit-width. Generally, where not otherwise noted, operations are designed to return exact mathematically-based answers. For binary operations, BigInts act as two's complement binary strings, with negative numbers treated as having bits set infinitely to the left.

The BigInt type in JavaScript only supports integers, not floating-point numbers. It is designed to represent integer values with arbitrary magnitude, allowing for safe storage of large integers that exceed the range supported by the Number data type.

The BigInt type is created by appending n to the end of an integer or by calling the BigInt() function.

let x = 123456789012345678901234567890n;
let y = BigInt("123456789012345678901234567890");

BigInt values are neither always more precise nor always less precise than numbers, since BigInts cannot represent fractional numbers, but can represent big integers more accurately. Neither type entails the other, and they are not mutually substitutable. A TypeError is thrown if BigInt values are mixed with regular numbers in arithmetic expressions, or if they are implicitly converted to each other.

String Type

The String type represents textual data and is encoded as a sequence of 16-bit unsigned integer values representing UTF-16 code units. Each element in the string occupies a position in the string. The first element is at index 0, the next at index 1, and so on. The length of a string is the number of UTF-16 code units in it, which may not correspond to the actual number of Unicode characters

JavaScript strings are immutable. This means that once a string is created, it is not possible to modify it. String methods create new strings based on the content of the current string — for example:

  • A substring of the original using substring().
  • A concatenation of two strings using the concatenation operator (+) or concat().
const string1 = "A string primitive";
const string2 = 'Also a string primitive';
const string3 = `Yet another string primitive`;

An empty string has both a legal value and a type.

let str = "";    // The value is "", the typeof is "string"
Symbol type

The Symbol type is the set of all non-String values that may be used as the key of an Object property.

Each possible Symbol value is unique and immutable.

Each Symbol value immutably holds an associated value called description that is either undefined or a String value.

To create a new primitive symbol, you write Symbol() with an optional string as its description:

const sym1 = Symbol();
const sym2 = Symbol("foo");
const sym3 = Symbol("foo");

The above code creates three new symbols. Note that Symbol("foo") does not coerce the string "foo" into a symbol. It creates a new symbol each time:

Symbol("foo") === Symbol("foo"); // false
Object Type

An Object is logically a collection of properties. Each property has corresponding attributes. Each attribute is accessed internally by the JavaScript engine, but you can set them through Object.defineProperty(), or read them through Object.getOwnPropertyDescriptor(). Each property is either a data property, or an accessor property:

  • A data property associates a key with a value.
  • An accessor property associates a key value with one or two accessor functions (get and set) to retrieve or store a value.

Properties are identified using key values. A property key value is either a String value or a Symbol value. All String and Symbol values, including the empty String, are valid as property keys. A property name is a property key that is a String value.

Property keys are used to access properties and their values. There are two kinds of access for properties: get and set, corresponding to value retrieval and assignment, respectively. The properties accessible via get and set access includes both own properties that are a direct part of an object and inherited properties which are provided by another associated object via a property inheritance relationship. Inherited properties may be either own or inherited properties of the associated object. Each own property of an object must each have a key value that is distinct from the key values of the other own properties of that object.

Every object property has a set of invisible attributes containing metadata associated with that property, called property descriptors.

There are two types of descriptors associated with any property: data descriptors and accessor descriptors. A data descriptor includes key and value pairs that contain a property's value, regardless of whether that value is writable, configurable, or enumerable. Accessor descriptors contain functions that execute when a property is set, changed, or accessed.

The following table lists all object property attributes.

Attribute Name Types of property Data Type Default Value Description
value data property any data type undefined The value retrieved by a get access of the property.
writable data property boolean false determines if the value of a data property can be changed. If false, attempts to change the property's value attribute using set will not succeed.
get accessor property object or undefined undefined If the value is an object, it must be a function object. The function's call internal method is called with an empty arguments list to retrieve the property value each time a get access of the property is performed.
set accessor property object or undefined undefined If the value is an object, it must be a function object. The function's call internal method is called with an arguments list containing the assigned value as its sole argument each time a set access of the property is performed. The effect of a property's set internal method may, but is not required to, have an effect on the value returned by subsequent calls to the property's get internal method.
enumerable data property or accessor property boolean false If true, the property will be enumerated by a for-in enumeration. Otherwise, the property is said to be non-enumerable.
configurable data property or accessor property boolean false If false, attempts to delete the property, change it from a data property to an accessor property or from an accessor property to a data property, or make any changes to its attributes (other than replacing an existing value or setting writable to false) will fail.

Every object property is more than just a name and value pair. Each property is having property descriptor that helps to see all the attributes of that property.

The following code retrieves the property descriptor for the data property firstName:

const person = {
    firstName:"John", 
    lastName:"Doe", 
    age:50
};
console.log(Object.getOwnPropertyDescriptor(person, "firstName"));
// Object { value: "John", writable: true, enumerable: true, configurable: true }

In the above example, besides the value attribute, the data property firstName has three more attributes such as configurable, enumerable and writable.

writable attribute decides whether the value associated with the property can be changed or not, from its initial value.

By default the properties of the object are writable that means we can change its value.

const person = {
    firstName:"John", 
    lastName:"Doe", 
    age:50
};
person.firstName = "Donald";
person.lastName = "Duck";
console.log(person.firstName);
// Output: "Donald"
console.log(person.lastName);
// Output: "Duck"

Now let's make the property firstName read only. Change the writable attribute of property firstName to false.

const person = {
    firstName:"John", 
    lastName:"Doe", 
    age:50
};
person.firstName = "Donald";
// Throws error in strict mode
// Uncaught TypeError: Cannot assign to read only property
console.log(person.firstName);
// Output: "John"

By default the properties of the object are enumerable that means we can enumerate them with for..in loop or Object.keys() method.

const person = {
    firstName:"John", 
    lastName:"Doe", 
    age:50
};
for(const property in person) { 
    const item = `${property}: ${person[property]}`; 
    console.log(item);
}
// firstName: John
// lastName: Doe
// age: 50
console.log(Object.keys(person));
// Array(3) ["firstName", "lastName", "age"]

Now let's make the property firstName ignored in iteration using for..in loop or Object.keys() method. Change the enumerable attribute of property firstName to false.

Object.defineProperty(person, "firstName", { enumerable: false });
for(const property in person) { 
    const item = `${property}: ${person[property]}`; 
    console.log(item);
}
// lastName: Doe
// age: 50
console.log(Object.keys(person));
// Array(2) ["lastName", "age"]

By default the attributes of the properties of the object can be changed. By making the attributes of the property non-configurable you can not change the attributes of the property further.

Now let's make the property firstName non-configurable. Change the configurable attribute of property firstName to false.

const person = {
    firstName:"John", 
    lastName:"Doe", 
    age:50
};
Object.defineProperty(person, "firstName", { configurable: false });
// let's change the attribute configurable again
Object.defineProperty(person, "firstName", { configurable: true });
// Uncaught TypeError: can't redefine non-configurable property "firstName"

The get and set attributes in property descriptors are functions that serve as getter and setter methods for a property, respectively. The get attribute is called when the property is accessed, and the set attribute is called when the property is modified. These attributes are used to define accessor properties, which are a type of property that provides a way to customize the behavior of getting and setting a property's value.

In the next example, we define an object person with two private properties _firstName and _lastName. The property name _firstName and _lastName have a getter and setter function. The getter function returns the value of the associated property, and the setter function sets the value of the associated property to the input value.

const person = {
    _firstName:"John", 
    _lastName:"Doe", 
    age:50,
    set firstName(name) {
        this._firstName = name;
    },
    get firstName() {
        return this._firstName;
    },
    set lastName(name) {
        this._lastName = name;
    },
    get lastName() {
        return this._lastName;
    }
};
person.firstName = "Donald";
person.lastName = "Duck";
console.log(person.firstName);
// "Donald"
console.log(person.lastName);
// "Duck"