Skip to the chapter
←JavaScriptGGroundworkJSJavaScript
JavaScript/Advanced/A4
32 / 41 written chapters
/

The cover
  1. B1Setup & mental model
  2. B2Execution context
  3. B3var, let & const
  4. B4Scope
  5. B5Single thread
  6. B6In the browser
  7. B7Types & values
  8. B8Strings & Math
  9. B9Operators & flow
  10. B10Functions
  11. B11Objects & arrays
  12. B12this
  13. B13Closures
  14. B14DOM & events
  15. B15Callbacks & promises
  16. B16Errors & tools
  17. B17Guided project
  18. ★Cheat page
  1. I1Scope & functions, properly
  2. I2Objects deeply
  3. I3Prototypes & OOP
  4. I4Async, properly
  5. I5Modules & tooling
  6. I6Regex & dates
  7. I7Error handling
  8. I8Real-time connections
  9. I9Offline & storage
  10. I10Browser APIs
  11. I11Modern JS
  12. ★Cheat page
  1. A1Engine & memory
  2. A2Advanced async
  3. A3Metaprogramming
  4. A4Types & data
  5. A5Patterns & architecture
  6. A6Performance
  7. A7Security
  8. A8Ecosystem
  9. A9Testing
  10. A10Observability
  11. A11Interview bank
  12. A12Build it yourself
  13. ★Cheat page

JSA4 · Part 3 · Advanced

Types & data

What happens once a number, a string, or a file gets big or exotic enough to need its own type.

5 min read6 sections2 exercises
32 of 41

In this chapter

  • BigInt — exact integers, past 2^53
  • ArrayBuffer, TypedArrays, DataView
  • TextEncoder, TextDecoder, and base64 the built-in way
  • Blob, File, FileReader
  • Unicode — code points vs code units

and 1 more

about 5 min
06On this page
  1. BigInt — exact integers, past 2^53
  2. ArrayBuffer, TypedArrays, DataView
  3. TextEncoder, TextDecoder, and base64 the built-in way
  4. Blob, File, FileReader
  5. Unicode — code points vs code units
  6. Intl — past basic formatting

BigInt — exact integers, past 2^53

Every regular JS number is a 64-bit float, which means integers stop being exactly representable past Number.MAX_SAFE_INTEGER — 2^53 - 1. BigInt is a genuinely separate type for arbitrary-size integers with no such ceiling, spelled with a trailing n.

console.log(Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2);   // what happens?
console.log(9007199254740991n + 1n === 9007199254740991n + 2n);              // same numbers, as BigInt — what happens?

true, then false. Past the safe integer limit, regular numbers genuinely can't tell MAX_SAFE_INTEGER + 1 and + 2 apart — both round to the same closest representable float. The n suffix versions stay exact, because BigInt isn't a float at all.

⚠ BigInt and Number don't mix 10n + 5 throws TypeError: Cannot mix BigInt and other types — there's no implicit conversion between them in either direction. Convert explicitly, one way or the other: 10n + BigInt(5) or Number(10n) + 5.

ArrayBuffer, TypedArrays, DataView

An ArrayBuffer is a fixed-length block of raw bytes — nothing more, no way to read or write it directly. A TypedArray (Int32Array, Uint8Array, etc.) is a typed view onto that same memory, interpreting its bytes as a specific numeric type. Multiple views can share one buffer at once, and writing through any of them changes the same underlying bytes every other view sees.

const buffer = new ArrayBuffer(4);       // 4 raw bytes
const asInt32 = new Int32Array(buffer);  // one view: "these 4 bytes are one 32-bit int"
asInt32[0] = 42;

const dv = new DataView(buffer);          // a second, more manual view of the SAME bytes
console.log(dv.getInt32(0));              // what happens?
console.log(dv.getInt32(0, true));        // second argument: littleEndian — what happens?

704643072, then 42. Not a bug — a genuine byte-order mismatch. TypedArrays use the platform's native byte order (little-endian on essentially every real device today). DataView.getInt32 defaults to big-endian unless you explicitly pass true for its second argument. Same 4 bytes, same buffer, two different interpretations of what order they represent a number in — exactly the kind of detail that matters the moment you're parsing a binary file format or a network protocol that specifies its own byte order.

TextEncoder, TextDecoder, and base64 the built-in way

const bytes = new TextEncoder().encode("café");   // a Uint8Array — always UTF-8, no other option
console.log(bytes.length);                          // what happens?

const text = new TextDecoder().decode(bytes);
console.log(text);                                   // what happens?

5, then "café" — bytes.length is 5, not 4, because "é" takes two bytes once it's UTF-8 encoded, a different count entirely from either the code-unit or code-point counts covered next in this chapter. TextEncoder/TextDecoder are the bridge every binary API in this chapter eventually needs — fetch bodies, WebSocket frames, and crypto.subtle all work in bytes, not strings.

bytes.toBase64();                       // "Y2Fmw6k=" — no more manual btoa/Uint8Array dance
Uint8Array.fromBase64("Y2Fmw6k=");      // back to real bytes

Before these existed, converting bytes to base64 text meant a clumsy detour through btoa and a manually-built string of character codes — btoa only ever accepted text, not raw bytes, and broke outright on anything outside Latin-1. toBase64()/fromBase64() work directly on the bytes themselves; encoding is no longer a text operation in disguise.

Blob, File, FileReader

const blob = new Blob(["hello world"], { type: "text/plain" });
blob.size;    // 11 — bytes, not characters (matters once text isn't plain ASCII)
blob.type;    // "text/plain"

// A File (from an <input type="file"> or a drop event) is a Blob with a name and a modified date
fileInput.addEventListener("change", (e) => {
  const file = e.target.files[0];
  const reader = new FileReader();
  reader.onload = () => console.log(reader.result);   // the fully-read contents
  reader.readAsText(file);          // or readAsArrayBuffer, readAsDataURL
});

// modern alternative — same result, promise-based, no event wiring
const text = await file.text();
const bytes = await file.arrayBuffer();

FileReader predates promises; a File object itself now has .text()/.arrayBuffer() methods that return promises directly — same underlying read, no callback wiring needed in new code.

Unicode — code points vs code units

A JS string's .length counts UTF-16 code units, not visible characters. Most characters fit in one 16-bit unit; a large chunk of emoji and some rarer scripts need two units — a surrogate pair — and .length counts both of them as 2.

const emoji = "😀";
console.log(emoji.length);            // what happens?
console.log([...emoji].length);       // spreading iterates by CODE POINT, not code unit — what happens?
console.log(JSON.stringify(emoji[0])); // indexing still grabs one code UNIT — what happens?

2, then 1, then "\ud83d" — half of a surrogate pair, not a valid character on its own. emoji[0] silently cuts an emoji in half; spreading a string (or for...of, or Array.from) walks it by actual code point and never splits a surrogate pair. Slicing a string by raw index — a search-result excerpt, a truncated preview — risks exactly this cut, and it's an easy one to never notice until a specific emoji or script breaks in production.

Code points still aren't what a reader calls a character. An emoji like 👨‍👩‍👧 is five code points — three people glued together with invisible zero-width joiners — so spreading it gives five pieces. Counting what a person actually sees needs Intl.Segmenter, which splits by grapheme:

const family = "👨‍👩‍👧";
family.length;                // 8 — code units
[...family].length;           // 5 — code points
const graphemes = new Intl.Segmenter("en", { granularity: "grapheme" });
[...graphemes.segment(family)].length;   // 1 — what the reader sees

Two strings can also look identical and still be different code points underneath: "é" can be one code point, or a plain "e" followed by a separate combining accent mark.

const composed = "caf\u00e9";      // é as ONE code point
const decomposed = "cafe\u0301";   // e + a combining accent

composed === decomposed;                                    // false — yet both render as "café"
composed.length + " vs " + decomposed.length;               // "4 vs 5"
composed.normalize("NFC") === decomposed.normalize("NFC");  // true — one canonical spelling
Rule Comparing user-typed text for equality without .normalize() first is a real, if rare, bug — two strings can render pixel-identical and still fail ===, if one came from a source that encodes accents differently.

Dry run: measuring the same string three different ways

String.length (UTF-16 code units)[...str].length (code points)Intl.Segmenter graphemes (what a reader sees)
"😀"211
"👨‍👩‍👧" (family, joined by zero-width joiners)851
"café" (one composed é)444

Three different numbers for the same three strings, and each answers a different question: .length counts UTF-16 storage units (what a naive slice would split on), spreading counts actual Unicode code points, and Intl.Segmenter counts what a person would actually call "one character." Only the third number matches human intuition — which is exactly why slicing user-facing text by .length or raw index risks cutting a character in half.

Intl — past basic formatting

["café", "cafe", "cafz"].sort(new Intl.Collator("en").compare);
// ["cafe", "café", "cafz"] — locale-aware ordering; a plain .sort() compares raw code
// points instead, which gets accented characters and non-Latin scripts sorted wrong

new Intl.RelativeTimeFormat("en").format(-1, "day");   // "1 day ago"
new Intl.RelativeTimeFormat("en").format(3, "hour");   // "in 3 hours"

All four Intl constructors from this and earlier chapters — Collator, DateTimeFormat, NumberFormat, and RelativeTimeFormat — take the same first argument, a locale string, and are the built-in answer to "format this correctly for the reader's language and region" without hand-writing rules that differ by country.

Interview answer, one sentence

"BigInt gives exact integers beyond 2^53, typed arrays over an ArrayBuffer give raw binary data, and strings are UTF-16 code units, so length and indexing can split a character that takes more than one unit."

Before you move on
  • Explain why BigInt and Number can't be mixed with +, and how to convert between them explicitly.
  • Say why a TypedArray view and a DataView can disagree on the same 4 bytes, and which argument fixes it.
  • Explain why bytes.length from a TextEncoder differs from a string's own .length for non-ASCII text.
  • Walk through why "😀".length is 2 but [..."😀"].length is 1, and what a surrogate pair is.
  • Explain why composed "café" and decomposed "cafe" + combining accent fail === but pass after .normalize("NFC").

02Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 10 testsA factorial that doesn't lose precisionadvanced
  • 10 testsCount actual characters, not code unitsadvanced
All 31 for this level →
A4

Finished reading?

0 of 41 chapters read in this series

→Next stop

  1. A3Previous · A3Metaprogramming
  2. A4you are here
  3. Next · A5Patterns & architectureShapes that show up again and again once code has to scale past one file.7 min read

01On this page

6 sections · 0%

  1. 01BigInt — exact integers, past 2^53
  2. 02ArrayBuffer, TypedArrays, DataView
  3. 03TextEncoder, TextDecoder, and base64 the built-in way
  4. 04Blob, File, FileReader
  5. 05Unicode — code points vs code units
  6. 06Intl — past basic formatting

[] chapters · t top