# Data types

Source: https://codewiki.com/javascript/data-types/

> - **what**: JavaScript is dynamically typed: bindings don't have fixed types, but values do. Language values divide into seven primitive types and objects.
> - **trap**: Operators and conditions apply their own coercion rules: `"false"` is truthy, `Number("")` is `0`, and `+` can add or concatenate.
> - **fix**: Parse and validate each field by its meaning at input boundaries, distinguish nullish values from other falsy values when defaulting, and choose equality semantics deliberately.

## What it is and why it exists

JavaScript uses dynamic typing: runtime values have types, while variable names don't. A `let` binding can point to a number and later point to a string. The original value hasn't changed type; the binding now points to another value. Dynamic typing lets the same syntax handle data from forms, JSON, and library calls, but code must state its type assumptions at those boundaries.

Language values divide into objects and seven primitive values: `undefined`, `null`, Boolean, Number, BigInt, String, and Symbol. Primitive values are immutable; replacing text or changing its case produces a new string. Objects can have mutable properties, and arrays and functions are objects rather than additional language types.

Assignment and argument passing both pass values. Copying a primitive gives you that primitive value. Copying an object value gives you another reference to the same object, so two bindings can observe changes to the same properties. Explaining this as "primitives live on the stack and objects live on the heap" isn't reliable because storage is an implementation strategy, not ECMAScript semantics.

`const` constrains a binding: it can't be made to point at another value, but it doesn't freeze an object. `const order = { count: 1 }` still permits changing `order.count`. If an API needs a read-only object contract, express that through its design, copying, or freezing; don't infer deep immutability from `const`.

These types most often affect business results when code checks external input, selects operator semantics, or decides whether two values are equal. The problem usually isn't that "JavaScript has no types." It is that code has left the input type, conversion rule, or missing-value policy implicit.

## How it works

### Seven primitive types and objects

`undefined` often means that no value was supplied, as with a missing property or a function without an explicit return. `null` is an empty value used explicitly by the program. An interface decides whether the two have different domain meanings; the language only specifies that they are distinct primitive values.

Boolean has only `true` and `false`. String is a sequence of UTF-16 code units. Symbol is a primitive with unique identity that can also serve as a property key. Number uses the IEEE 754 double-precision floating-point format for ordinary integers, fractions, `Infinity`, and `NaN`.

BigInt represents arbitrary-precision integers, and its literals end in `n`. It is a different numeric type from Number. Most arithmetic operations reject a mixture because the runtime can't decide whether to preserve BigInt precision or use Number's range and fractional semantics. Before comparing or converting, decide which numeric representation the domain needs.

Object is the type of every other language value. Plain objects, arrays, dates, regular expressions, maps, sets, and functions all belong here. Separate objects have separate identities even when their properties match; two references pass strict equality only when they point to the same object.

### `typeof` is a coarse check

`typeof` returns one of a fixed set of strings. It is useful for distinguishing primitive types and detecting callable values, but it isn't a general object classifier. The table shows common results and the checks to add when you need more precision.

| Value | `typeof` result | When a more precise check is needed |
| --- | --- | --- |
| `undefined` | `"undefined"` | Usually no extra check |
| `null` | `"object"` | `value === null` |
| Boolean | `"boolean"` | Usually no extra check |
| Number | `"number"` | `Number.isFinite()`, `Number.isNaN()` |
| BigInt | `"bigint"` | Usually no extra check |
| String | `"string"` | Usually no extra check |
| Symbol | `"symbol"` | Usually no extra check |
| Function | `"function"` | Check the specific protocol when necessary |
| Arrays and other objects | `"object"` | `Array.isArray()` or a domain check |

`typeof null === "object"` is compatibility behavior retained by the language. Arrays also return `"object"`, so an array branch must use `Array.isArray(value)`. A "non-null object" check requires at least `value !== null && typeof value === "object"`; after that, validate the needed properties rather than treating every object as the same kind of record.

### Conversion depends on context

Type coercion is the automatic conversion an operation performs when it needs another type. Conditions and `!` use Boolean conversion; most arithmetic uses numeric conversion; interpolation and `String()` use string conversion. Calling `Boolean()`, `Number()`, or `String()` explicitly selects the corresponding rules, but "explicit" doesn't mean the result matches your domain.

Boolean conversion has a fixed set of falsy values: `false`, `0`, `-0`, `0n`, `""`, `null`, `undefined`, and `NaN`. Every other language value is truthy, including `"0"`, `"false"`, empty arrays, and empty objects. `if (value)` answers "what is its truth value?" It is not a substitute for "is this field present?" or "is this array empty?"

`&&` and `||` test operand truth values but return an original operand rather than a Boolean. `left && right` returns `left` when it is falsy and otherwise returns `right`; `left || right` returns `left` when it is truthy and otherwise returns `right`. `??` chooses the right side only when the left is `null` or `undefined`, so it preserves a valid `0`, `false`, or empty string.

Numeric conversion also has results worth defending against at boundaries. `Number("")` and `Number("   ")` are both `0`, and `Number(null)` is `0`; invalid numeric text produces `NaN`. `parseInt()` parses a prefix, so `parseInt("12px", 10)` is `12`. That suits a syntax that explicitly permits a suffix, not validation that the whole field is a decimal integer.

### Operators choose the coercion path

Subtraction, multiplication, division, and remainder first convert operands to numeric values. Addition first converts objects to primitives; if either result is a String, it concatenates, and otherwise it performs numeric addition. This makes `1 + 2 + "3"` equal `"33"`, while `"1" + 2 + 3` is `"123"`, because the expression associates from left to right.

Relational comparison can't be reduced to "convert everything to numbers" either. After both operands become primitives, two strings are compared by their UTF-16 code units; otherwise comparison takes the numeric path. As a result, `"10" < "2"` is `true`, while `Number("10") < Number("2")` is `false`.

Loose equality `==` uses abstract equality comparison. It may coerce operands, but it doesn't simply "convert both sides to the same type." For example, `null == undefined` is `true`, while loose comparisons between either one and other values are normally false. A Boolean is first converted to Number; an object compared with a primitive is first converted to a primitive. Except for an intentional `value == null` check that matches both nullish values, business code is usually easier to review with strict equality and explicit boundary conversion.

### Equality is not one algorithm

Strict equality `===` doesn't coerce. Different types are unequal, objects compare by identity, `NaN` is unequal to itself, and `+0` equals `-0`. It is the default choice for most branches, but it doesn't provide structural comparison for objects with matching content.

`Object.is()` uses SameValue semantics: `Object.is(NaN, NaN)` is `true`, while `Object.is(+0, -0)` is `false`. `Array.prototype.includes()`, Map keys, and Set members use SameValueZero. It considers `NaN` equal to itself and the two zeros equal. When you choose a collection or lookup API, these boundary differences directly affect whether a value is found.

## Examples

The four examples inspect types, parse external configuration, observe operator coercion, and compare equality semantics. Every output shown came from running the corresponding file locally with Node 24.

### Inspecting language types

This probe starts with `typeof` and adds dedicated checks for `null` and arrays. It doesn't try to manufacture one "exact type name" for every built-in object because application code usually needs to validate behavior or fields, not trust a spoofable label.

<!-- quick -->

```javascript
// file: inspect_values.js
const samples = [
  undefined,
  null,
  false,
  42,
  42n,
  "42",
  Symbol("id"),
  [42],
  { value: 42 },
  () => 42,
];

for (const value of samples) {
  const kind = value === null
    ? "null"
    : Array.isArray(value)
      ? "array"
      : typeof value;
  console.log(kind);
}
```

```text
undefined
null
boolean
number
bigint
string
symbol
array
object
function
```


<!-- /quick -->

Here, `kind` is an application-level classification, not a new ECMAScript type system. It deliberately answers "is this null, an array, or another `typeof` category?" If a function accepts only a limited structure, continue by checking own properties and their value types.

### Parsing configuration at a boundary

Environment variables, URL parameters, and form fields commonly enter a program as strings. This parser accepts only the spellings in its contract, rejects partial numbers, and uses `??` to preserve an empty label.

```javascript
// file: parse_options.js
function parseBoolean(value, field) {
  if (value === true || value === "true") return true;
  if (value === false || value === "false") return false;
  throw new TypeError(`${field} must be true or false`);
}

function parseRetries(value) {
  const text = String(value);
  if (!/^(0|[1-9]\d*)$/.test(text)) {
    throw new TypeError("retries must be a non-negative integer");
  }
  const retries = Number(text);
  if (!Number.isSafeInteger(retries)) {
    throw new RangeError("retries is outside the safe integer range");
  }
  return retries;
}

function parseOptions(raw) {
  return {
    retries: parseRetries(raw.retries),
    verbose: parseBoolean(raw.verbose, "verbose"),
    label: raw.label ?? "worker",
  };
}

console.log(parseOptions({ retries: "0", verbose: "false", label: "" }));
console.log(parseOptions({ retries: 3, verbose: true }));
```

```text
{ retries: 0, verbose: false, label: '' }
{ retries: 3, verbose: true, label: 'worker' }
```

`Number.isSafeInteger()` ensures the result is a safe integer, not a Number that has already been rounded. Parsing policy is part of the interface. If the contract permits surrounding whitespace, a leading plus, or localized digits, add those rules to the expression and tests instead of asking a conversion function to guess.

### Observing contextual conversion

The same string can take different paths under different operators. `JSON.stringify()` makes empty strings and spaces within strings visible in the output, so terminal rendering doesn't hide the result.

```javascript
// file: coercion_contexts.js
const inputs = ["5", "", "false", 0, null];

for (const value of inputs) {
  console.log(JSON.stringify({
    input: value,
    boolean: Boolean(value),
    number: Number(value),
    defaultWithOr: value || "fallback",
    defaultWithNullish: value ?? "fallback",
  }));
}

console.log("5" + 2);
console.log("5" - 2);
console.log(1 + 2 + "3");
```

```text
{"input":"5","boolean":true,"number":5,"defaultWithOr":"5","defaultWithNullish":"5"}
{"input":"","boolean":false,"number":0,"defaultWithOr":"fallback","defaultWithNullish":""}
{"input":"false","boolean":true,"number":null,"defaultWithOr":"false","defaultWithNullish":"false"}
{"input":0,"boolean":false,"number":0,"defaultWithOr":"fallback","defaultWithNullish":0}
{"input":null,"boolean":false,"number":0,"defaultWithOr":"fallback","defaultWithNullish":"fallback"}
52
3
33
```

The third line shows `null` for `number`. That isn't because `Number("false")` returns `null`: it returns `NaN`, and JSON serializes a non-finite Number in an array or object as `null`. When diagnosing conversion, check `Number.isNaN()` directly instead of treating serialized text as the in-memory value.

### Comparing equality and object identity

This example puts strict equality, SameValue, and SameValueZero together. The final two lines show that matching object content doesn't imply matching object identity.

```javascript
// file: compare_values.js
console.log(NaN === NaN);
console.log(Object.is(NaN, NaN));
console.log([NaN].includes(NaN));

console.log(+0 === -0);
console.log(Object.is(+0, -0));
console.log(new Set([+0, -0]).size);

const first = { id: 7 };
const alias = first;
const copy = { id: 7 };

console.log(first === alias);
console.log(first === copy);
```

```text
false
true
true
true
false
1
true
false
```

If the domain considers records with the same `id` to represent one entity, compare validated `id` values directly. General deep comparison has to define cycles, prototypes, accessors, Symbol keys, and domain rules. `JSON.stringify(a) === JSON.stringify(b)` isn't a reliable substitute.

## Pitfalls

### Treating `typeof` as a complete classifier

> **Pitfall:** `typeof value === "object"` matches `null`, arrays, and many built-in objects. Reading a property immediately makes the `null` branch throw, while treating an array as a record can silently produce the wrong result.

**Fix:** exclude `null` first and use `Array.isArray()` for arrays. Then validate required fields, property ownership, and value types from the interface rather than inferring a full shape from one coarse label.

### Parsing Boolean text with `Boolean()`

> **Pitfall:** `Boolean("false")` and `Boolean("0")` are both `true` because any non-empty string is truthy. Generated configuration parsers often pass environment variables straight to `Boolean()`, enabling a feature that should be off.

**Fix:** define the accepted spellings at the boundary, such as exactly `"true"` and `"false"`, and reject everything else. Use `Boolean()` only when the input is already a language value and its truth value is what the operation needs.

### Letting `||` overwrite every falsy value

> **Pitfall:** `count || 10` changes a valid `0` to `10`, `enabled || true` changes `false` to `true`, and `label || "default"` discards a meaningful empty string.

**Fix:** use `??` when missing means only `null` or `undefined`. If an empty string or zero is also missing in this domain, write a named validation condition so the business rule remains visible.

### Equating successful conversion with valid input

> **Pitfall:** `Number("")` is `0`, while `parseInt("12px", 10)` is `12`. Checking only that the result isn't `NaN` accepts empty or suffixed text outside the contract.

**Fix:** validate the complete input syntax before conversion, then check the converted range. For integers, choose `Number.isInteger()` or `Number.isSafeInteger()` according to the domain. Avoid global `isNaN()`, which coerces its argument first.

### Forgetting the two meanings of `+`

> **Pitfall:** Adding a form value of `"20"` to the number `5` produces `"205"`. When one side is BigInt and the other is Number, numeric addition throws `TypeError` instead of choosing a representation automatically.

**Fix:** parse both sides into the same numeric type at the calculation boundary. Use a template literal when constructing a message so string intent is visible; don't borrow addition with an empty string as a conversion tool.

### Confusing value equality with object content

> **Pitfall:** `===` compares object identity rather than walking properties, and `NaN === NaN` is false. Conversely, coercion makes `0 == false` true without proving that the two fields represent the same domain value.

**Fix:** default to `===` in ordinary branches. Use `Number.isNaN()` for `NaN`, follow the documented membership semantics of a collection API, and compare defined keys or implement contract-specific structural comparison for domain objects.

<!-- deep -->

## Bindings, mutation, and wrapper objects

A binding and the value currently stored in it are separate parts of the model. Reassigning `let current = 1` to `current = 2` changes the binding, not the Number value `1`. Mutation applies to an object's properties or internal state instead. This distinction explains why a `const` object may be mutable while a String held by `let` remains immutable.

| Operation | Binding changes | Existing value mutates |
| --- | --- | --- |
| `current = other` | Yes | No |
| `record.count = 2` | No | Yes, if the write succeeds |
| `text.toUpperCase()` | No | No; it returns a new String value |
| `items.push(value)` | No | Yes; the Array object changes |
| `Object.freeze(record)` | No | The object becomes shallowly non-writable through ordinary data properties |

Property access on a primitive can appear object-like because JavaScript temporarily supplies access to wrapper behavior. That is why `"hi".toUpperCase()` works even though a String value isn't an object. The temporary mechanism doesn't turn the original primitive into a mutable record.

Calling `String(value)`, `Number(value)`, or `Boolean(value)` without `new` performs conversion. Constructing with `new String()`, `new Number()`, or `new Boolean()` creates a wrapper object instead. Every object is truthy, so `new Boolean(false)` is truthy and is almost never the value an application intends.

Wrapper objects also fail strict equality against their primitive payloads: `new Number(3) !== 3`. Accept primitives at ordinary API boundaries unless object identity is explicitly part of the contract. To extract a wrapper's payload when integrating with legacy code, call `value.valueOf()` and then validate its primitive type.

`Object.freeze()` is shallow: it prevents ordinary changes to the frozen object's own properties, but a nested object may remain mutable. Freezing is an object policy and has no role in making an already immutable primitive "more immutable."

Functions are callable objects even though `typeof` reports `"function"` for convenience. They can own properties and compare by identity just like other objects; the special result doesn't create a ninth language type.

## Object-to-primitive conversion

When syntax needs a primitive but receives an object, it runs the specification's `ToPrimitive` abstract operation. If the object provides `[Symbol.toPrimitive](hint)`, the runtime calls it first. It must return a primitive or throw `TypeError`. The `hint` can be `"number"`, `"string"`, or `"default"`; it describes what the syntax prefers rather than requiring a particular primitive type.

Without a custom hook, ordinary conversion tries `valueOf()` and `toString()` in an order based on the hint. The string hint normally tries `toString()` first; other hints normally try `valueOf()` first, and conversion stops only when a method returns a primitive. Date has special treatment for the default hint, so "adding an empty string to an object always calls `valueOf()` first" is not a general rule.

Addition applies primitive conversion to both operands. If either converted value is a String, it converts both to String and concatenates; otherwise it converts both to numeric values and adds. A custom `Symbol.toPrimitive` can therefore affect logging, templates, comparison, and arithmetic. If its answer varies with hidden state, call sites become hard to reason about, so keep the hook simple and give it clear domain meaning.

Symbol string conversion has another easy-to-miss distinction. `String(symbol)` returns descriptive text, but implicit concatenation and template interpolation throw `TypeError` for a Symbol value. Diagnostic code that accepts string or Symbol keys should call `String(key)` explicitly.

## Equality boundaries

ECMAScript APIs don't all call the same equality relation. The table summarizes four common semantics. Every row compares objects by identity and does not inspect their properties automatically.

| Algorithm or entry point | Coerces | `NaN` equals itself | `+0` equals `-0` | Common entry point |
| --- | --- | --- | --- | --- |
| Abstract equality | Yes | No | Yes | `==` |
| Strict equality | No | No | Yes | `===`, `indexOf()` |
| SameValue | No | Yes | No | `Object.is()` |
| SameValueZero | No | Yes | Yes | `includes()`, Map, Set |

Loose equality isn't random, but it has many branches, and object conversion may invoke user code. `value == null` is a common narrow idiom that matches only `null` and `undefined`. If a team permits it, preserve the intent with a lint exception and a comment. Other cross-type comparisons are usually easier to review after explicit boundary conversion followed by strict equality.

`switch` matching uses strict equality, so `case NaN` can never match `NaN`. Array `indexOf(NaN)` also can't find `NaN`, while `includes(NaN)` can. Tests for lookup behavior should call the production API rather than substitute another comparison that looks equivalent.

## Number, BigInt, and serialization boundaries

Number can represent integers exactly only within a limited range. `Number.isSafeInteger()` tells you whether an integer still has an unambiguous representation. Consecutive integers outside that range can round to the same Number, so converting with `Number(largeText)` and then testing safety can reject an unsafe result, but it can't recover lost digits. Construct a BigInt directly from the original text when large integers are required.

BigInt doesn't represent fractions and can't mix with Number in most arithmetic. Relational comparison can compare the two numeric types, but explicit unification makes the precision policy easier to see. Converting an arbitrary Number to BigInt also requires that Number to be an integer; if it was already outside the safe range, the conversion precisely preserves the rounded Number.

Default `JSON.stringify()` doesn't support BigInt and throws `TypeError`. An interface must decide whether to encode large integers as decimal strings, restrict them to safe Numbers, or use another protocol with a defined integer type. After JSON is parsed, inspecting a Number can't tell you whether the sender had already lost precision, so the contract must choose a representation before transport.

`NaN`, `Infinity`, and `-Infinity` are Number values, but JSON writes them as `null` when they occur as object properties or array elements. A JSON round trip therefore can't preserve every JavaScript Number. If non-finite values matter, reject them before serialization or define explicit tagged representations.

<!-- /deep -->

[Checkpoint: javascript/data-types](https://codewiki.com/javascript/data-types/#checkpoint)

## Further reading

- [MDN: JavaScript data types and data structures](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Data_structures)
- [MDN: `typeof`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof)
- [MDN: Equality comparisons and sameness](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Equality_comparisons_and_sameness)
- [ECMAScript specification: language types and values](https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html)
- [ECMAScript specification: `ToPrimitive`](https://tc39.es/ecma262/multipage/abstract-operations.html#sec-toprimitive)
