JavaScript Basics: Store Numeric Data in Typed Arrays
Typed arrays store numeric elements in a contiguous backing buffer. Choose an element type when you need a defined numeric representation, then be explicit about copying, sharing, and conversion. These examples use numeric lists rather than mixed-field binary records.
These examples were tested on Linux with Node.js v24.19.0. They use built-in APIs only, require no network access, and include assertions that check the results. See MDN’s typed-array reference and the ECMAScript typed-array specification. All buffers created here have fixed sizes.
Prepare a scratch directory
You need Bash, a text editor, Node.js 24 on your PATH, and the mktemp, rm, and rmdir utilities. Keep using the same terminal. Save each named source file as UTF-8; .mjs selects ES modules. Each example runs independently.
node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1The tested runtime prints:
v24.19.01. Allocate and populate a numeric block
Save this as store-numeric-counts.mjs:
import assert from 'node:assert/strict';
const counts = new Uint16Array(4);
assert.deepEqual(Array.from(counts), [0, 0, 0, 0]);
counts.set([12, 20, 8], 1);
assert.deepEqual(Array.from(counts), [0, 12, 20, 8]);
assert.equal(counts.length, 4);
assert.equal(counts.byteLength, 8);
assert.equal(counts.buffer.byteLength, 8);
assert.equal(counts.reduce((sum, value) => sum + value, 0), 40);
assert.throws(() => counts.set([1, 2], 3), RangeError);
assert.equal(new Uint16Array(0).length, 0);
console.log(`Counts: ${counts.join(', ')}`);
console.log(`Elements: ${counts.length}; bytes: ${counts.byteLength}`);
console.log(`Total: ${counts.reduce((sum, value) => sum + value, 0)}`);Run it:
node store-numeric-counts.mjsOutput:
Counts: 0, 12, 20, 8
Elements: 4; bytes: 8
Total: 40Uint16Array uses two bytes for each unsigned integer element and starts with zeroed storage. The set() offset counts elements, not bytes. Its range check prevents the two-element copy from fitting at offset 3. This allocation cannot grow with push(); choose a regular array for a general-purpose growable list.
2. Choose a shared window or an independent copy
Save this as compare-numeric-views.mjs:
import assert from 'node:assert/strict';
const readings = new Float64Array([12.5, 15, 17.5, 20]);
const window = readings.subarray(1, 3);
const copy = readings.slice(1, 3);
assert.equal(window.buffer, readings.buffer);
assert.notEqual(copy.buffer, readings.buffer);
assert.equal(window.byteOffset, Float64Array.BYTES_PER_ELEMENT);
window[0] = 99;
copy[1] = -1;
assert.deepEqual(Array.from(readings), [12.5, 99, 17.5, 20]);
assert.deepEqual(Array.from(window), [99, 17.5]);
assert.deepEqual(Array.from(copy), [15, -1]);
assert.equal(readings.subarray(2, 2).length, 0);
console.log(`Original: ${readings.join(', ')}`);
console.log(`Window: ${window.join(', ')}`);
console.log(`Copy: ${copy.join(', ')}`);Run it:
node compare-numeric-views.mjsOutput:
Original: 12.5, 99, 17.5, 20
Window: 99, 17.5
Copy: 15, -1Both methods use element indices with an exclusive end. subarray() creates a view of existing storage, so a write through the window changes the original. slice() creates new storage. A small shared window also keeps its entire backing buffer alive; make a copy when independence or releasing the larger allocation matters.
3. Validate values before narrowing their type
Save this as check-byte-conversions.mjs:
import assert from 'node:assert/strict';
const values = [255, 256, -1, 3.9];
const wrapped = Uint8Array.from(values);
const clamped = Uint8ClampedArray.from(values);
assert.deepEqual(Array.from(wrapped), [255, 0, 255, 3]);
assert.deepEqual(Array.from(clamped), [255, 255, 0, 4]);
function checkedBytes(values) {
if (!values.every(value => Number.isInteger(value) && value >= 0 && value <= 255)) {
throw new RangeError('Expected integer bytes from 0 through 255');
}
return Uint8Array.from(values);
}
assert.deepEqual(Array.from(checkedBytes([0, 128, 255])), [0, 128, 255]);
assert.equal(checkedBytes([]).length, 0);
for (const value of [-1, 256, 3.9, NaN, Infinity, '3']) {
assert.throws(() => checkedBytes([value]), RangeError);
}
console.log(`Uint8Array: ${wrapped.join(', ')}`);
console.log(`Uint8ClampedArray: ${clamped.join(', ')}`);
console.log('Invalid byte values rejected before conversion');Run it:
node check-byte-conversions.mjsOutput:
Uint8Array: 255, 0, 255, 3
Uint8ClampedArray: 255, 255, 0, 4
Invalid byte values rejected before conversionConversion is not validation. For these finite inputs, Uint8Array truncates fractional parts and wraps integers into its 0–255 range. Uint8ClampedArray clamps out-of-range values and rounds fractions; exact half-way cases use round-to-even, as defined by ECMAScript’s clamping operation. The helper instead rejects invalid values before conversion. It expects a dense array of candidate values. For serialized multibyte formats with a specified byte order, use DataView rather than assuming a typed array’s native byte order.
Clean up
In the same terminal, remove only the named example files, return to your original directory, and remove the empty scratch directory:
rm -- store-numeric-counts.mjs compare-numeric-views.mjs check-byte-conversions.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirAcknowledgements
This post was created with assistance from AI.