`DataView` in TypeScript
This article explains DataView in TypeScript.
We will explain DataView in TypeScript, from the basics to practical usage, step by step.
YouTube Video
DataView in TypeScript
By using DataView in TypeScript, you can perform fine-grained byte-level read and write operations on an ArrayBuffer. DataView is extremely useful for low-level binary processing such as protocol implementation, binary file analysis, and sending/receiving binary data over WebSocket.
Basic Concepts: Differences between ArrayBuffer, TypedArray, and DataView
ArrayBuffer is a fundamental data structure used to store a fixed-length sequence of bytes. TypedArrays such as Uint8Array are views that treat the buffer as an array of a specific type, where each element has a fixed type.
On the other hand, DataView is a flexible view that allows you to read and write values of any data type at any offset position. Because it does not assume fixed types and allows fine-grained control at the byte level, it is suitable for analyzing binary protocols and low-level processing.
The following is an example of creating an ArrayBuffer and then creating a Uint8Array and DataView from it.
1// Create an ArrayBuffer (8 bytes)
2const buffer = new ArrayBuffer(8);
3
4// Create a Uint8Array view (easy to manipulate individual bytes)
5const u8 = new Uint8Array(buffer);
6u8[0] = 0x12;
7u8[1] = 0x34;
8
9// Create a DataView (allows reading/writing any type at any offset)
10const dv = new DataView(buffer);
11
12// Read a 16-bit unsigned integer (big-endian assumed if second argument is omitted)
13console.log(dv.getUint16(0)); // 0x1234
- In this code, two different types of views—
Uint8ArrayandDataView—are used simultaneously on a single buffer. UsingDataView, you can flexibly read and write values by specifying arbitrary offsets and endianness.
Main Methods of DataView
DataView is an interface for manipulating an ArrayBuffer at the byte level, and provides methods to read and write various types such as integers and floating-point numbers.
The main methods are as follows:.
- Read:
getUint8,getInt8,getUint16,getInt16,getUint32,getInt32,getFloat32,getFloat64 - Write:
setUint8,setInt8,setUint16,setInt16,setUint32,setInt32,setFloat32,setFloat64
All of these methods take a 'byte offset' as the first argument: get reads the value at that position, and set writes the value to that position. You can also specify the endianness with the second argument when handling 16-, 32-, or 64-bit data. In practice, it is safest to always specify the endianness according to the data specification.
The following example shows how to write a 32-bit integer and a 32-bit floating-point number in little endian format to a buffer and then read them back in the same format.
1const buf = new ArrayBuffer(12); // Enough space for integer + float
2const view = new DataView(buf);
3
4// Write a 32-bit unsigned integer at offset 0 (little-endian)
5view.setUint32(0, 305419896, true); // 305419896 = 0x12345678
6
7// Read the same 32-bit unsigned integer back from offset 0 (little-endian)
8const intVal = view.getUint32(0, true);
9console.log(intVal); // 305419896
10
11// Write a 32-bit floating number at offset 4 (little-endian)
12view.setFloat32(4, 3.1415926, true);
13
14// Read the 32-bit floating number back from offset 4 (little-endian)
15const floatVal = view.getFloat32(4, true);
16console.log(floatVal); // 3.1415926 (approx)
- By explicitly specifying the endianness, you can ensure compatibility with different platforms and binary specifications.
About Endianness (Byte Order)
Some protocols, such as those used in networking, use big-endian, while many CPUs and file formats predominantly use little-endian design. In DataView, if the second argument is set to true, it treats the data as little endian; if set to false or omitted, it treats it as big endian.
In the following example, you can see how the byte array in memory changes when writing the same number using both big-endian and little-endian formats.
1const b = new ArrayBuffer(4);
2const a = new Uint8Array(b);
3const dv = new DataView(b);
4
5// Write in big-endian order
6dv.setUint32(0, 0x0A0B0C0D, false); // big-endian
7console.log([...a]); // [10, 11, 12, 13]
8
9// Write the same value in little-endian order
10dv.setUint32(0, 0x0A0B0C0D, true); // little-endian
11console.log([...a]); // [13, 12, 11, 10]
- Understanding how the byte order changes between big endian and little endian will make it much easier to analyze communication data or binary formats.
String Input/Output (Using TextEncoder and Decoder Together)
DataView is excellent for reading and writing numbers, but it does not handle strings directly. It is common to encode strings using TextEncoder or TextDecoder (such as to UTF-8), then copy them into the buffer via Uint8Array. The following is an example of writing a UTF-8 string to a buffer and then reading it back.
1const encoder = new TextEncoder();
2const decoder = new TextDecoder();
3
4const str = "\u3053\u3093\u306b\u3061\u306f";
5const encoded = encoder.encode(str); // Uint8Array (UTF-8 encoded bytes)
6
7// Create a buffer and write the encoded string bytes into it
8const buf2 = new ArrayBuffer(encoded.length);
9const u8v = new Uint8Array(buf2);
10u8v.set(encoded);
11
12// Read the bytes and decode them back into a string
13const decodedStr = decoder.decode(new Uint8Array(buf2));
14console.log(decodedStr);- By converting strings to binary and, if necessary, prepending their length, you can store variable-length strings.
Practical Example: Encoding/Decoding a Custom Binary Format
Below, we define a simple message format containing a version number, ID, and name. We implement an encoding process to convert messages to binary data, and a decoding process to restore the original object from the binary. In this message format, the first byte stores the version, the next 4 bytes store the ID in little-endian format, the following byte contains the name length, and finally, the UTF-8 encoded name is stored.
1// Encode an object into a binary message
2function encodeMessage(msg: { version: number; id: number; name: string }): ArrayBuffer {
3 const encoder = new TextEncoder();
4 const nameBytes = encoder.encode(msg.name);
5 const total = 1 + 4 + 1 + nameBytes.length;
6 const buf = new ArrayBuffer(total);
7 const dv = new DataView(buf);
8 let offset = 0;
9
10 dv.setUint8(offset, msg.version); offset += 1; // write version
11 dv.setUint32(offset, msg.id, true); offset += 4; // write ID (little-endian)
12 dv.setUint8(offset, nameBytes.length); offset += 1; // write name length
13
14 // write name bytes
15 new Uint8Array(buf, offset).set(nameBytes);
16 return buf;
17}
18
19// Decode a binary message back into an object
20function decodeMessage(buf: ArrayBuffer) {
21 const dv = new DataView(buf);
22 let offset = 0;
23
24 const version = dv.getUint8(offset); offset += 1; // read version
25 const id = dv.getUint32(offset, true); offset += 4; // read ID (little-endian)
26 const nameLen = dv.getUint8(offset); offset += 1; // read name length
27
28 // extract name bytes
29 const nameBytes = new Uint8Array(buf, offset, nameLen);
30 const decoder = new TextDecoder();
31 // decode UTF-8 string
32 const name = decoder.decode(nameBytes);
33
34 return { version, id, name };
35}
36
37// Example usage
38const packed = encodeMessage({ version: 1, id: 42, name: "Alice" });
39const unpacked = decodeMessage(packed);
40console.log(unpacked); // { version: 1, id: 42, name: "Alice" }
- This is a typical implementation for handling a basic message format containing a variable-length string, and can be applied in many practical situations such as network communication or custom binary file designs.
Cautions and Best Practices
When working with binary data using DataView, there are several practical points to consider beyond simply reading and writing values—such as safety, consistent use of endianness, and proper handling of types. In particular, when handling binary data received from external sources or dealing with large integers, it is important to design your code to prevent incorrect reading and buffer overruns. Below are some useful points to remember for practical use.
-
Boundary Check
DataViewthrows an exception if an offset or size exceeds the buffer boundary. Always check lengths when dealing with untrusted binary data. -
Always Specify Endianness Always explicitly and consistently specify little endian or big endian in your code.
-
Type Consistency JavaScript numbers are 64-bit IEEE-754 floating-point values. Methods like
getUint32are handled appropriately, but there is nogetUint64, so special handling is required for 64-bit integers. -
Handling 64-bit Integers To handle 64-bit integers, you need to use
BigIntor manually split the value into high and low 32-bit parts. Here is a simple example of reading a 64-bit unsigned integer.
1function getUint64(dv: DataView, offset: number, littleEndian = true): bigint {
2 const low = BigInt(dv.getUint32(offset, littleEndian));
3 const high = BigInt(dv.getUint32(offset + 4, littleEndian));
4 return littleEndian ? (high << 32n) | low : (low << 32n) | high;
5}- By using
BigInt, you can safely handle integers larger than 64 bits as well.
Usage in Node.js (Interoperating with Buffer)
Although Buffer is commonly used in Node.js, it is also easy to convert between ArrayBuffer and DataView. Use the buffer property of Buffer objects or the Uint8Array constructor for conversion.
1// Node.js: Buffer -> ArrayBuffer
2const nodeBuf = Buffer.from([1,2,3,4]);
3const arrBuf = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.byteLength);
4const dvNode = new DataView(arrBuf);
5console.log(dvNode.getUint16(0));- This can be used as a tool for exchanging binary data between Node.js and browsers.
Summary
DataView is a powerful mechanism for freely reading and writing binary data—particularly useful in situations where flexible control such as specifying endianness and accessing arbitrary positions is needed. By combining ArrayBuffer, TypedArray, and DataView, you can flexibly and accurately handle binary data with TypeScript, enabling a wide range of use cases from protocol implementation to file analysis. By also incorporating string encoding and handling of 64-bit integers as needed, you can achieve even more practical binary operations.
You can follow along with the above article using Visual Studio Code on our YouTube channel. Please also check out the YouTube channel.