ArrayBuffer in TypeScript

ArrayBuffer in TypeScript

This article explains ArrayBuffer in TypeScript.

We will explain ArrayBuffer in TypeScript step-by-step, covering basics to practical techniques.

YouTube Video

ArrayBuffer in TypeScript

ArrayBuffer is a built-in object that represents a “raw memory area” for binary data. It represents a raw buffer of fixed length, upon which TypedArray or DataView are layered for reading and writing.

Basic Concepts and Characteristics of ArrayBuffer

ArrayBuffer is a fixed-length byte sequence. You specify the size in bytes when creating it, and its length cannot be changed afterward.

1// Create a new ArrayBuffer of 16 bytes.
2const buf = new ArrayBuffer(16);
3console.log(buf.byteLength); // 16
  • This code creates an empty buffer of 16 bytes. You can check the size using byteLength.

TypedArray and DataView — Views for Manipulating Buffers

ArrayBuffer does not have the ability to read or write data. Therefore, actual operations are performed through TypedArray or DataView, specifying types such as integers or floating-point numbers and endianness when accessing data.

1// Create a buffer and a Uint8Array view over it. Then write bytes and read them.
2const buffer = new ArrayBuffer(8);
3const u8 = new Uint8Array(buffer);
4
5u8[0] = 0x41; // 'A'
6u8[1] = 0x42; // 'B'
7console.log(u8); // Uint8Array(8) [ 65, 66, 0, 0, 0, 0, 0, 0 ]
  • Uint8Array is a byte-wise array view, and can be accessed like a normal array. If you place multiple views on the same ArrayBuffer, they share the same memory for reading and writing.

DataView: Reading and Writing at Arbitrary Boundaries and Endianness

DataView is useful for fine-grained reading and writing by byte, and allows you to specify little or big endian.

 1// Using DataView to write/read multi-byte values with endianness control.
 2const buf2 = new ArrayBuffer(8);
 3const view = new DataView(buf2);
 4
 5// Write a 32-bit integer (little-endian)
 6view.setInt32(0, 0x12345678, true);
 7
 8// Read it back as little-endian and big-endian
 9const little = view.getInt32(0, true);
10const big = view.getInt32(0, false);
11console.log(little.toString(16)); // "12345678"
12console.log(big.toString(16));    // "78563412"
  • DataView allows you to read and write values by specifying offsets in memory, making it suitable for implementing protocols that require handling network byte order.

Conversion between Strings and ArrayBuffer (TextEncoder / TextDecoder)

To convert text to binary and vice versa, use TextEncoder and TextDecoder.

 1// Convert string -> ArrayBuffer and back using TextEncoder/TextDecoder.
 2const encoder = new TextEncoder();
 3const decoder = new TextDecoder();
 4
 5// Unicode escape sequences
 6const str = "\u3053\u3093\u306B\u3061\u306F";
 7const encoded = encoder.encode(str); // Uint8Array
 8console.log(encoded); // Uint8Array([...])
 9
10// If you need an ArrayBuffer specifically:
11const ab = encoded.buffer.slice(encoded.byteOffset, encoded.byteOffset + encoded.byteLength);
12console.log(ab.byteLength); // bytes length of encoded text
13
14// Decode back
15const decoded = decoder.decode(encoded);
16console.log(decoded);
  • TextEncoder.encode returns a Uint8Array, so if you need an ArrayBuffer, you should refer to its .buffer property. With the slice method, you can extract the required data by specifying the offset and length.

Slicing and Copying ArrayBuffer

The slice method returns a new ArrayBuffer. ArrayBuffer cannot be resized; if resizing is necessary, create a new buffer and copy the data into it.

 1// Slice an ArrayBuffer and copy to a new sized buffer.
 2const original = new Uint8Array([1,2,3,4,5]).buffer;
 3const part = original.slice(1, 4); // bytes 1..3
 4console.log(new Uint8Array(part)); // Uint8Array [ 2, 3, 4 ]
 5
 6// Resize: create a new buffer and copy existing content
 7const larger = new ArrayBuffer(10);
 8const target = new Uint8Array(larger);
 9target.set(new Uint8Array(original), 0);
10console.log(target); // first bytes filled with original data
  • Since the slice method returns a new ArrayBuffer, if you prioritize efficiency or sharing references, you can use the subarray method of TypedArray to refer to the same buffer.

Difference between TypedArray.subarray and Copying

TypedArray's subarray returns a new view that refers to the same ArrayBuffer.

1// subarray shares the same underlying buffer; modifying one affects the other.
2const arr = new Uint8Array([10,20,30,40]);
3const viewSub = arr.subarray(1,3); // shares memory
4viewSub[0] = 99;
5console.log(arr); // Uint8Array [10, 99, 30, 40]
  • Shared views can avoid the cost of copying, but since they refer to the same buffer, you need to be careful about side effects.

Concatenating Buffers (Combining Multiple ArrayBuffers)

Since ArrayBuffer is of fixed length, to combine multiple buffers create a new ArrayBuffer and copy the data.

 1// Concatenate multiple ArrayBuffers
 2function concatBuffers(buffers: ArrayBuffer[]): ArrayBuffer {
 3  const total = buffers.reduce((sum, b) => sum + b.byteLength, 0);
 4  const result = new Uint8Array(total);
 5  let offset = 0;
 6  for (const b of buffers) {
 7    const u8 = new Uint8Array(b);
 8    result.set(u8, offset);
 9    offset += u8.length;
10  }
11  return result.buffer;
12}
13
14const a = new Uint8Array([1,2]).buffer;
15const b = new Uint8Array([3,4,5]).buffer;
16const c = concatBuffers([a, b]);
17console.log(new Uint8Array(c)); // [1,2,3,4,5]
  • If you frequently concatenate large amounts of data, precomputing the total size and allocating a new buffer only once, as in this code, is more efficient.

Passing ArrayBuffer to a Worker (Transferable)

In the browser's postMessage, you can transfer an ArrayBuffer as a "transferable" object. Ownership can be transferred without copying, which avoids the cost of copying.

1// Example: posting an ArrayBuffer to a Worker as a transferable object (browser)
2const worker = new Worker('worker.js');
3const bufferToSend = new Uint8Array([1,2,3,4]).buffer;
4
5// Transfer ownership to the worker (main thread no longer owns it)
6worker.postMessage(bufferToSend, [bufferToSend]);
7
8// After transfer, bufferToSend.byteLength === 0 in many browsers (detached)
9console.log(bufferToSend.byteLength); // may be 0
  • By specifying the objects to transfer in an array as the second argument of postMessage, you can transfer ownership, without copying, of transferable objects such as ArrayBuffer.
  • After transferring, the buffer becomes "detached" on the original side and cannot be accessed. Using SharedArrayBuffer allows simultaneous access from multiple threads, but its use comes with security requirements and environment restrictions.

Handling in Node.js (Interconversion with Buffer)

In Node.js, you can convert between the Node.js binary type Buffer and ArrayBuffer. This is useful when targeting both browsers and Node.js with TypeScript.

 1// Convert ArrayBuffer <-> Node.js Buffer
 2// In Node.js environment:
 3const ab = new Uint8Array([10,20,30]).buffer;
 4const nodeBuffer = Buffer.from(ab); // ArrayBuffer -> Buffer
 5console.log(nodeBuffer); // <Buffer 0a 14 1e>
 6
 7const backToAb = nodeBuffer.buffer.slice(
 8    nodeBuffer.byteOffset,
 9    nodeBuffer.byteOffset + nodeBuffer.byteLength
10);
11console.log(new Uint8Array(backToAb)); // Uint8Array [10,20,30]
  • Buffer.from(arrayBuffer) in Node.js usually creates a copy, but there are cases where reference sharing is possible, so be mindful of offsets.

Performance Considerations and Best Practices

To optimize performance and handle ArrayBuffer efficiently, there are several important points to keep in mind. Below, we list and explain practical best practices.

  • Reduce the Number of Copies When handling large binaries, use subarray (shared view) or transferables to reduce copies.

  • Allocate Large Buffers at Once Allocating small buffers repeatedly increases overhead. If possible, allocate a large buffer at once and use portions of it as needed.

  • Specify Endianness Explicitly When handling multibyte values, use DataView and explicitly specify endianness. Big-endian is often standard for network protocols.

Common Use Case Examples

ArrayBuffer is widely used both in browsers and Node.js.

  1. Parsing and building binary protocols (processing header information with DataView)
  2. Media processing such as image and audio data (fetch(...).then(res => res.arrayBuffer()))
  3. Shared memory for WebAssembly (Wasm memory operates based on ArrayBuffer)
  4. Offloading heavy processing to Workers (passing ArrayBuffer as transferable)

The following code is an example of acquiring and analyzing binary data.

1// Example: fetch binary data in browser and inspect first bytes
2async function fetchAndInspect(url: string) {
3  const resp = await fetch(url);
4  const ab = await resp.arrayBuffer();
5  const u8 = new Uint8Array(ab, 0, Math.min(16, ab.byteLength));
6  console.log('first bytes:', u8);
7}
  • This code fetches binary data from any URL in the browser and displays the first few bytes. The code loads data fetched via the fetch API as an ArrayBuffer and inspects the first 16 bytes with a Uint8Array.

Summary

ArrayBuffer is a raw memory representation, enabling efficient binary operations via TypedArray and DataView. By designing to avoid unnecessary copying and explicitly specifying endianness, you can achieve safe and high-performance binary processing.

You can follow along with the above article using Visual Studio Code on our YouTube channel. Please also check out the YouTube channel.

YouTube Video