Eight types — seven primitives and the object¶
Level: 101 · for anyone who has written a line of JavaScript
One line: Every value has one of eight types — seven primitives (undefined, null, boolean, number, bigint, string, symbol) and Object — and Object(v) === v is true only for an object; a primitive takes no property and never changes, while arrays, functions, dates, regular expressions and the instances of your own classes are kinds of Object, not types of their own.
Keywords: primitive, data types, immutable
The specification names eight types, and every value belongs to exactly one of them. The program below takes the twelve values this page asks about — undefined, null, true, 1, 1n, "s", Symbol(), {}, [], a function, a Date and a regular expression — and sorts them with a single test, Object(v) === v, without using typeof.
The program
eight_types_classify_js.js in full — pasted here by tools/run_examples.py from the file CI runs.
// Twelve values, sorted the way the language sorts them. Object(v) hands back
// v itself only when v is already an object; for a primitive it has to make a
// new object, so Object(v) === v is the test. No typeof anywhere.
function classify(value) {
const made = Object(value);
if (made === value) {
return { same: true, made: "the same object", is: "object", type: "Object" };
}
if (Object.getPrototypeOf(made) === Object.prototype) {
// Only undefined and null get a new, empty, ordinary object.
return { same: false, made: "a new empty object", is: "primitive", type: value === null ? "Null" : "Undefined" };
}
// Every other primitive gets a wrapper, named after its type.
const wrapper = made.constructor.name;
return { same: false, made: `a ${wrapper} wrapper`, is: "primitive", type: wrapper };
}
const samples = [
["undefined", undefined],
["null", null],
["true", true],
["1", 1],
["1n", 1n],
['"s"', "s"],
["Symbol()", Symbol()],
["{}", {}],
["[]", []],
["() => {}", () => {}],
["new Date(0)", new Date(0)],
["/s/", /s/],
];
console.log(`${"value".padEnd(13)}${"Object(v) === v".padEnd(17)}${"Object(v) returns".padEnd(21)}${"so it is".padEnd(11)}type`);
const types = [];
let primitives = 0;
for (const [label, value] of samples) {
const row = classify(value);
// For an object, name the constructor that made it: a kind of object, not a type.
const type = row.same ? `Object, made by ${value.constructor.name}` : row.type;
console.log(`${label.padEnd(13)}${String(row.same).padEnd(17)}${row.made.padEnd(21)}${row.is.padEnd(11)}${type}`);
if (!types.includes(row.type)) types.push(row.type);
if (!row.same) primitives += 1;
}
console.log(`${samples.length} values: ${primitives} primitives, ${samples.length - primitives} objects`);
console.log(`${types.length} types: ${types.join(", ")}`);
Verified output of eight_types_classify_js.js — regenerated by tools/run_examples.py, never hand-typed.
value Object(v) === v Object(v) returns so it is type
undefined false a new empty object primitive Undefined
null false a new empty object primitive Null
true false a Boolean wrapper primitive Boolean
1 false a Number wrapper primitive Number
1n false a BigInt wrapper primitive BigInt
"s" false a String wrapper primitive String
Symbol() false a Symbol wrapper primitive Symbol
{} true the same object object Object, made by Object
[] true the same object object Object, made by Array
() => {} true the same object object Object, made by Function
new Date(0) true the same object object Object, made by Date
/s/ true the same object object Object, made by RegExp
12 values: 7 primitives, 5 objects
8 types: Undefined, Null, Boolean, Number, BigInt, String, Symbol, Object
Reading the output¶
- The test.
Object(v)turnsvinto an object. Whenvis already an object there is nothing to do, and it returnsvitself, so the comparison istrue: the last five rows. For a primitive it must return some other value, an object, so the comparison isfalse: the first seven rows. Those are the steps ECMA-262 gives forObject ( value )↗. undefinedandnullget a new, empty, ordinary object: they are the two primitives with no wrapper (MDN — Primitive values ↗). The program names their types by comparing withundefinedandnulldirectly.- The other five primitives each get a wrapper object, and the wrapper's constructor carries the name of the type:
Boolean,Number,BigInt,String,Symbol. A property read on a primitive, such as"s".length, is looked up on a wrapper like these (MDN — Primitive ↗); Primitives and wrappers is the page about that. {},[], the arrow function, theDateand the regular expression are all of one type, Object. What differs is the constructor that made each of them —Object,Array,Function,Date,RegExp— so an array or a function is a kind of object, not a type.- The last two lines count the rows: seven primitives and five objects, spread over eight types. That is the whole of ECMA-262's list: Undefined, Null, Boolean, String, Symbol, Number, BigInt and Object (ECMA-262 — ECMAScript language types ↗), where a primitive value is a member of any of the first seven (ECMA-262 — primitive value ↗).
What can an object do that a string cannot?¶
Three things: hold a property of its own, change in place, and be told apart from another object with the same contents. The program tries each on an object, order, and on a string, code. The file is an ES module, and module code always runs in strict mode, where an assignment that cannot happen throws a TypeError (MDN — Strict mode for modules ↗); the program catches each error and prints its message.
The program
eight_types_immutable_primitives_js.js in full — pasted here by tools/run_examples.py from the file CI runs.
// Three things an object can do, tried on an object and then on a string.
// This file is an ES module, so it runs in strict mode: an assignment that
// cannot happen throws a TypeError instead of being ignored.
function attempt(label, action) {
let result;
try {
result = action();
} catch (error) {
result = `${error.name}: ${error.message}`;
}
console.log(` ${label.padEnd(28)}${result}`);
}
const order = { code: "ab-100" }; // an object
let code = "ab-100"; // a string, one of the seven primitives
console.log("1. hold a property of its own");
attempt('order.note = "fragile"', () => {
order.note = "fragile";
return `order.note is "${order.note}"`;
});
attempt('code.note = "fragile"', () => {
code.note = "fragile";
return `code.note is "${code.note}"`;
});
console.log("2. change in place");
attempt('order.code = "AB-100"', () => {
order.code = "AB-100";
return `order is ${JSON.stringify(order)}`;
});
attempt('code[0] = "A"', () => {
code[0] = "A";
return `code is "${code}"`;
});
attempt("code.toUpperCase()", () => `returns "${code.toUpperCase()}"; code is still "${code}"`);
attempt("code = code.toUpperCase()", () => {
code = code.toUpperCase();
return `code is "${code}": a new string in the same variable`;
});
console.log("3. be compared by identity");
const first = { code: "ab-100" };
const second = { code: "ab-100" };
attempt("first === second", () => `${first === second}: two objects, built alike`);
attempt("first === first", () => `${first === first}: one object`);
attempt('"ab-100" === "ab-" + "100"', () => `${"ab-100" === "ab-" + "100"}: two strings, same characters`);
Verified output of eight_types_immutable_primitives_js.js — regenerated by tools/run_examples.py, never hand-typed.
1. hold a property of its own
order.note = "fragile" order.note is "fragile"
code.note = "fragile" TypeError: Cannot create property 'note' on string 'ab-100'
2. change in place
order.code = "AB-100" order is {"code":"AB-100","note":"fragile"}
code[0] = "A" TypeError: Cannot assign to read only property '0' of string 'ab-100'
code.toUpperCase() returns "AB-100"; code is still "ab-100"
code = code.toUpperCase() code is "AB-100": a new string in the same variable
3. be compared by identity
first === second false: two objects, built alike
first === first true: one object
"ab-100" === "ab-" + "100" true: two strings, same characters
- 1. A property of its own. The object takes
note. The string refuses it withTypeError: Cannot create property 'note' on string 'ab-100': a primitive has nowhere to keep a property. In sloppy code the same assignment is ignored without a word instead, and the string still has nonote(MDN — Strict mode: setting properties on primitive values ↗). - 2. Change in place. The object changes, and keeps the
notefrom step 1.code[0] = "A"throws, because a string's characters are read-only properties (ECMA-262 — String exotic objects ↗).toUpperCase()leavescodealone as well: it returns a new string. What can change is the variable —code = code.toUpperCase()puts a new string into it, which is whycodeis declared withlet. MDN's glossary draws the same line between a primitive and a variable that holds one (MDN — Primitive ↗). - 3. Identity. Each object literal makes a new object, so
first === secondisfalsealthough the two look the same; an object is===only to itself. Two strings have no identity to tell apart:===compares their characters (ECMA-262 — SameValueNonNumber ↗), so"ab-100"equals"ab-" + "100". Values and references follows the difference into assignment and function calls.
Why are there two numeric types?¶
Until ES2020 there was one: the list of types in ES2019 has Number as its only numeric type, and BigInt joins the list in ES2020 (ES2019 — ECMAScript language types ↗, ES2020 — ECMAScript language types ↗). The program shows what a number cannot do and a bigint can.
eight_types_two_numbers_js.js in full — pasted here by tools/run_examples.py from the file CI runs.
// Two numeric types. A number is a 64-bit double, whole or not, and holds
// whole numbers exactly only up to 2 ** 53. A bigint stays exact past that,
// and holds whole numbers only.
const rows = [
["typeof 1", JSON.stringify(typeof 1)],
["typeof 1.5", JSON.stringify(typeof 1.5)],
["typeof 1n", JSON.stringify(typeof 1n)],
["1 === 1.0", String(1 === 1.0)],
["1 === 1n", String(1 === 1n)],
["Number.MAX_SAFE_INTEGER", String(Number.MAX_SAFE_INTEGER)],
["2 ** 53 + 1", String(2 ** 53 + 1)],
["2 ** 53 + 1 === 2 ** 53", String(2 ** 53 + 1 === 2 ** 53)],
["2n ** 53n + 1n", `${2n ** 53n + 1n}n`],
["2n ** 53n + 1n === 2n ** 53n", String(2n ** 53n + 1n === 2n ** 53n)],
["7 / 2", String(7 / 2)],
["7n / 2n", `${7n / 2n}n`],
];
for (const [expression, result] of rows) {
console.log(`${expression.padEnd(30)}${result}`);
}
Verified output of eight_types_two_numbers_js.js — regenerated by tools/run_examples.py, never hand-typed.
typeof 1 "number"
typeof 1.5 "number"
typeof 1n "bigint"
1 === 1.0 true
1 === 1n false
Number.MAX_SAFE_INTEGER 9007199254740991
2 ** 53 + 1 9007199254740992
2 ** 53 + 1 === 2 ** 53 true
2n ** 53n + 1n 9007199254740993n
2n ** 53n + 1n === 2n ** 53n false
7 / 2 3.5
7n / 2n 3n
typeofanswers"number"for1and for1.5. Whole or fractional, a number is one type, and1 === 1.0istruebecause1.0is the same value as1(Every number is a double).typeof 1nis"bigint", and1 === 1nisfalse:===isfalsewhenever the two types differ (ECMA-262 — IsStrictlyEqual ↗).- A number runs out of whole numbers. A number is a 64-bit IEEE 754 double (ECMA-262 — The Number Type ↗).
Number.MAX_SAFE_INTEGERis 9007199254740991, 2^53 − 1, and just past 2^53 the doubles skip a whole number:2 ** 53 + 1prints9007199254740992and is===to2 ** 53(MDN — Number.MAX_SAFE_INTEGER ↗). Safe integers maps where the gaps are. - A bigint stays exact past 2^53, and holds only whole numbers.
2n ** 53n + 1nprints9007199254740993nand differs from2n ** 53n;7 / 2is3.5, but7n / 2nis3n. What happens when the two types meet in one expression is the subject of BigInt.
Why can you not add a ninth type?¶
The list is part of the language, and only a new edition of the standard changes it: ES5.1 had six types, ES2015 added Symbol, and ES2020 added BigInt (ES5.1 ↗, ES2015 ↗, ES2020 ↗). Nothing a program writes adds to it. What a program writes instead is a class, so the program asks what a class makes, then runs the first program's test over a dozen other things a program can build.
The program
eight_types_no_ninth_js.js in full — pasted here by tools/run_examples.py from the file CI runs.
// A program cannot add a ninth type. What it writes instead is a class, and a
// class is a function that builds objects. Everything else a program can make
// passes the same test as {} did: Object(v) is v itself.
class Money {
constructor(cents, currency) {
this.cents = cents;
this.currency = currency;
}
}
const price = new Money(1999, "USD");
const facts = [
["typeof Money", JSON.stringify(typeof Money)],
["typeof price", JSON.stringify(typeof price)],
["Object.getPrototypeOf(price) === Money.prototype", String(Object.getPrototypeOf(price) === Money.prototype)],
];
for (const [expression, result] of facts) {
console.log(`${expression.padEnd(50)}${result}`);
}
console.log("");
const made = [
["Money", Money],
["price", price],
["new Map()", new Map()],
["new Set()", new Set()],
['new Error("out of stock")', new Error("out of stock")],
["Promise.resolve(price)", Promise.resolve(price)],
["new Proxy(price, {})", new Proxy(price, {})],
["new Uint8Array(4)", new Uint8Array(4)],
["Object.create(null)", Object.create(null)],
["Object(1999n)", Object(1999n)],
["Math", Math],
["globalThis", globalThis],
];
console.log(`${"value".padEnd(27)}Object(v) === v`);
let objects = 0;
for (const [label, value] of made) {
const isObject = Object(value) === value;
if (isObject) objects += 1;
console.log(`${label.padEnd(27)}${isObject}`);
}
console.log(`${objects} of ${made.length} are objects: one type, Object`);
Verified output of eight_types_no_ninth_js.js — regenerated by tools/run_examples.py, never hand-typed.
typeof Money "function"
typeof price "object"
Object.getPrototypeOf(price) === Money.prototype true
value Object(v) === v
Money true
price true
new Map() true
new Set() true
new Error("out of stock") true
Promise.resolve(price) true
new Proxy(price, {}) true
new Uint8Array(4) true
Object.create(null) true
Object(1999n) true
Math true
globalThis true
12 of 12 are objects: one type, Object
- The class is a function.
typeof Moneyis"function"; whytypeofgives callable objects an answer of their own is on thetypeofpage, and what else a class is, on Classes. - What it builds is an object.
typeof priceis"object", andprice's prototype isMoney.prototype. The class added a kind of object — one more link in a prototype chain — not a type. - Everything else is an object too. The class, its instance, a
Map, aSet, anError, aPromise, aProxy, a typed array, an object with no prototype at all, the wrapper object around a BigInt,Mathand the global object all pass the test that{}passed in the first program: twelve different kinds, one type.
Does tsc know a string is not an object?¶
It does, and it says so before anything runs. The program repeats one assignment from What can an object do that a string cannot?, then offers a string where TypeScript's type object is wanted.
eight_types_primitive_tserror.ts in full — pasted here by tools/run_examples.py from the file CI runs.
// tsc draws the same line before anything runs. It will not let a program
// change a character of a string, and it will not accept a string where the
// type `object` (any value that is not a primitive) is wanted.
let code = "ab-100";
code[0] = "A";
const order: object = { code };
const label: object = code;
What tsc says about eight_types_primitive_tserror.ts — regenerated by tools/run_examples.py, never hand-typed.
eight_types_primitive_tserror.ts:5:1 - error TS2542: Index signature in type 'String' only permits reading.
5 code[0] = "A";
~~~~~~~
eight_types_primitive_tserror.ts:8:7 - error TS2322: Type 'string' is not assignable to type 'object'.
8 const label: object = code;
~~~~~
Found 2 errors in the same file, starting at: eight_types_primitive_tserror.ts:5
- Line 5, TS2542.
code[0] = "A", which threw aTypeErrorwhen the second program ran, does not get past the check. The message saysStringwith a capital S althoughcodehas the typestring; Primitive and literal types is the page on that difference. - Line 8, TS2322. The TypeScript handbook defines
objectas any value that is not a primitive (TypeScript Handbook — More on Functions:object↗). Line 7 puts an object literal into it and passes; line 8 tries the string and fails. It is the lineObject(v) === vdraws, drawn at compile time.object,Objectand{}sets it beside two types that sound the same.
What to do¶
- To ask whether a value is a primitive, compare
Object(value) !== value; to ask for one particular type, usetypeoforvalue === null. - Treat a primitive as finished: to change a string, build a new one and assign it to the variable.
- Keep information about a primitive in an object beside it, never on the primitive itself.
- Use
===on two objects only when you mean "the same object"; to compare contents, compare their fields. - Use
bigintfor whole numbers that can pass 2^53, andnumberfor everything else. - Write a class when you need a new kind of object, and expect it to be of type Object like every other.
In other languages¶
- Ruby: Everything is an object, even
1andnil↗ — in Ruby, and nearly as much in Python (the page runs both),1andnilare objects with a class of their own; here seven of the eight types are primitives, and a primitive is not an object at all - Rust: The integer types ↗ — twelve integer types in Rust, where JavaScript has two numeric types
- Encodings: The shell has no string type ↗ — the shell has one type where JavaScript has eight
Practice¶
Kata: primitives on one line, objects on the other¶
Write isPrimitive(value) without typeof, and use it to sort these twelve values, kept in this order: 1999, "1999", 1999n, true, null, undefined, Symbol("sku"), [1999], { cents: 1999 }, () => 1999, new Date(0) and new Number(1999). Print each value as it is written in the source, so that the program prints exactly this:
Verified output of eight_types_kata_js.js — regenerated by tools/run_examples.py, never hand-typed.
primitives (7): 1999, "1999", 1999n, true, null, undefined, Symbol("sku")
objects (5): [1999], { cents: 1999 }, () => 1999, new Date(0), new Number(1999)
One of the twelve holds a number and is still an object.
Solution
eight_types_kata_js.js in full — pasted here by tools/run_examples.py from the file CI runs.
// Kata solution: sort twelve values into primitives and objects without typeof.
// Object(value) returns value itself only when value is already an object.
const isPrimitive = (value) => Object(value) !== value;
const values = [
["1999", 1999],
['"1999"', "1999"],
["1999n", 1999n],
["true", true],
["null", null],
["undefined", undefined],
['Symbol("sku")', Symbol("sku")],
["[1999]", [1999]],
["{ cents: 1999 }", { cents: 1999 }],
["() => 1999", () => 1999],
["new Date(0)", new Date(0)],
["new Number(1999)", new Number(1999)],
];
const primitives = values.filter(([, value]) => isPrimitive(value)).map(([label]) => label);
const objects = values.filter(([, value]) => !isPrimitive(value)).map(([label]) => label);
console.log(`primitives (${primitives.length}): ${primitives.join(", ")}`);
console.log(`objects (${objects.length}): ${objects.join(", ")}`);
Sources¶
- MDN — JavaScript data types and data structures ↗
- MDN — Primitive ↗
- MDN — Strict mode: setting properties on primitive values ↗
- MDN — Number.MAX_SAFE_INTEGER ↗
- ECMA-262 — ECMAScript Language Types ↗
- ECMA-262 — primitive value ↗
- ECMA-262 — Object ( value ) ↗
- ECMA-262 — String exotic objects ↗
- ECMA-262 — IsStrictlyEqual ↗
- ECMA-262 — The Number Type ↗
- The list of types in four editions: ES5.1 ↗, ES2015 ↗, ES2019 ↗, ES2020 ↗
- TypeScript Handbook — More on Functions:
object↗ - JavaScript: The Definitive Guide — David Flanagan, ch. 3 Types, Values, and Variables, and Eloquent JavaScript — Marijn Haverbeke, ch. 1 Values, Types, and Operators; both are on the Books page
See also¶
typeof— the operator that names the types, with one wrong answer- Primitives and wrappers — how a primitive gets methods it does not have
- Values and references — what assignment copies for each kind of value
- Primitive and literal types — the same primitives as TypeScript types
nullandundefined— the two primitives with no wrapper- Symbols — the primitive ES2015 added
- BigInt — the primitive ES2020 added, and what happens when it meets a number
- Safe integers — where a number stops holding every whole number
- Classes — what the class you write instead of a ninth type is
object,Objectand{}— TypeScript's type for "not a primitive", beside two that sound alike- Strict mode — why the string assignments throw here instead of doing nothing
- Errors around values and types — the messages this chapter's mistakes produce, each with its fix
- Lints around values and types — the checks that catch those mistakes before the code runs