Typed Arrays and Binary Data
Mental model: An ArrayBuffer is raw bytes; a typed array is a fixed-width window onto those bytes.
Level: advanced · about 10 minutes
const bytes = new Uint8Array(4); // 4 bytes, all zero
bytes[0] = 255;
bytes[1] = 256; // wraps to 0
bytes[2] = -1; // wraps to 255
bytes[3] = 3.7; // truncates to 3
console.log([...bytes]); // [255, 0, 255, 3]Values are clamped to the width you chose. No exceptions, no warnings.
A regular array can hold anything and grows on demand. A typed array holds numbers of one fixed width in a contiguous block of memory. You give up flexibility and get a layout that maps directly onto files, sockets, canvas pixels, audio samples and GPU buffers.
| Type | Bytes each | Range | Typical use |
|---|---|---|---|
Uint8Array | 1 | 0 to 255 | bytes, file and network data |
Int8Array | 1 | -128 to 127 | signed small integers |
Uint8ClampedArray | 1 | 0 to 255, clamped not wrapped | canvas pixel data |
Uint16Array / Int16Array | 2 | 0 to 65535 / -32768 to 32767 | audio samples, code units |
Uint32Array / Int32Array | 4 | up to about 4.29 billion | indices, colours, bitfields |
Float16Array | 2 | about 3 decimal digits | ML weights, compact GPU data (ES2025) |
Float32Array | 4 | about 7 decimal digits | geometry, WebGL, audio |
Float64Array | 8 | full JS number precision | scientific data |
BigInt64Array | 8 | 64-bit integers | timestamps, ids beyond 2**53 |
The buffer is the data; views are how you read it
ArrayBuffer(8) [ b0 b1 b2 b3 b4 b5 b6 b7 ]
│ │ │ │ │ │ │ │
Uint8Array(8) u0 u1 u2 u3 u4 u5 u6 u7
Uint32Array(2) └── word 0 ──┘└── word 1 ──┘
writing through one view changes what the other one reads
const buffer = new ArrayBuffer(4);
const asBytes = new Uint8Array(buffer);
const asWord = new Uint32Array(buffer);
asBytes[0] = 1;
asBytes[1] = 1;
console.log([...asBytes]); // [1, 1, 0, 0]
console.log(asWord[0]); // 257 on a little-endian machine
console.log(buffer.byteLength, asWord.length); // 4 1
const view = new DataView(new ArrayBuffer(2));
view.setUint16(0, 258, false); // false = big-endian
console.log([...new Uint8Array(view.buffer)]); // [1, 2]
console.log(view.getUint16(0, false)); // 258, read it back big-endian
console.log(view.getUint16(0, true)); // 513, same bytes, wrong assumptionDataView lets you choose the byte order per read and write.
const samples = new Float32Array([0.5, -0.25, 0.75]);
console.log(samples.map((v) => v * 2)); // Float32Array [1, -0.5, 1.5]
console.log(samples.filter((v) => v > 0)); // Float32Array [0.5, 0.75]
console.log(samples.subarray(1)); // a view, not a copy
console.log(Array.from(samples)); // back to a normal array
// samples.push(1), TypeError: length is fixedTyped arrays have most array methods, but not the ones that change length.
console.log(new Float64Array([0.1])[0]); // 0.1
console.log(new Float32Array([0.1])[0]); // 0.10000000149011612
console.log(new Float16Array([0.1])[0]); // 0.0999755859375
console.log(new Float16Array([1.5])[0]); // 1.5, exact, it is a power-of-two fraction
When you will actually need this
- Reading an uploaded file with
await file.arrayBuffer()and parsing a header. - Canvas pixel manipulation via
ctx.getImageData().data, which is aUint8ClampedArray. - WebGL or WebGPU vertex, index and texture buffers.
- Web Audio, where sample data is
Float32Array. - Streaming binary protocols over a WebSocket or
fetchbody. - Passing memory to WebAssembly, which only speaks bytes.
const a = new Uint8Array(2);
a[0] = 300;
a[1] = -1;
console.log(a[0], a[1], a.length);Uint8Array keeps the low 8 bits, so 300 wraps to 44 and -1 becomes 255 in two-s complement. Nothing throws, which is why silent corruption is the classic typed-array bug. Uint8ClampedArray would have given 255 and 0.
Try it yourself
One buffer, several views
const buffer = new ArrayBuffer(8);
const bytes = new Uint8Array(buffer);
const words = new Uint32Array(buffer);
const view = new DataView(buffer);
words[0] = 0x01020304;
console.log('bytes after a little-endian write:', [...bytes.subarray(0, 4)]);
view.setUint32(4, 0x01020304, false); // big-endian
console.log('bytes after a big-endian write:', [...bytes.subarray(4)]);
console.log('as hex:', [...bytes].map((b) => b.toString(16).padStart(2, '0')).join(' '));
Write through the Uint32Array and read the bytes back. Then redo it with a DataView in big-endian and compare the byte order.
Exercises
Bytes to hex
Write bytesToHex(bytes) that takes a Uint8Array and returns a lowercase hex string, two characters per byte, with no separator. An empty input gives an empty string.
Check yourself
- What does this log?
- 4 — Only the low 8 bits are kept, so
260(which is256 + 4) becomes4. Typed arrays never throw on out-of-range writes, which makes this a quiet class of bug. UseUint8ClampedArrayif you want255instead. - You are parsing a network packet whose fields are big-endian. Which tool do you use?
DataViewwith the little-endian flag set tofalse— Typed array views always use the host byte order, which is little-endian on the machines you ship to.DataViewlets you state the byte order per read, sogetUint16(0, false)reads big-endian regardless of the hardware.- Which is true of typed arrays?
- Their length is fixed at creation, and they have
mapandfilterbut nopush— A typed array is a fixed-width view over a fixed-size buffer, so nothing can change its length. It does carry the iteration methods, and each one returns a typed array of the same kind rather than a plain array.
Common mistakes
- Assuming an out-of-range write throws. It wraps (or clamps) silently.
- Reading multi-byte values with a typed array view and forgetting the host is little-endian.
- Expecting
Float32Arrayto round-trip a decimal exactly. It stores 32 bits, not 64.
Takeaways
- An
ArrayBufferis bytes; typed arrays andDataVieware views over those bytes. - Writes are wrapped or clamped to the element width, never rejected.
- Typed array views follow host byte order, so use
DataViewwhenever bytes cross a boundary. - Most application code never needs these. Recognise them, and reach for them only for real binary work.