Typed Arrays Fundamentals: Your First View into Binary Data
You've just learned about ArrayBuffer—a powerful tool that creates raw binary storage in JavaScript. You create your first ArrayBuffer, excited to start working with binary data:
const buffer = new ArrayBuffer(8);
console.log(buffer[0]); // undefined - wait, what?
buffer[0] = 255; // Try to write data
console.log(buffer[0]); // Still undefined - nothing happened!
What's going on? You've just discovered a fundamental truth about ArrayBuffer: it's storage without a door. You created the space, but you can't actually put anything in or take anything out.
This is exactly the problem Typed Arrays solve. They're the "door" that lets you access your ArrayBuffer. Think of it this way:
In this guide, you'll learn everything you need to understand Typed Arrays from the ground up. We'll use clear analogies, visual diagrams, and practical examples to make binary data feel natural and intuitive.
What You Need to Know First
Required reading:
- ArrayBuffer: Foundation of Binary Data - You must understand what ArrayBuffer is and why it exists before learning about Typed Arrays
Helpful background:
- Binary Number System - Understanding how computers store numbers will help you grasp why we need different "types" of arrays
- Basic understanding of how computer memory works (we'll explain the key concepts)
Basic requirements:
- JavaScript/TypeScript fundamentals: variables, functions, arrays, loops
- Familiarity with regular JavaScript arrays (e.g.,
[1, 2, 3]
) - Understanding that computers store everything as numbers (bytes)
If you're new to binary data:
Don't worry! We'll explain every concept with real-world analogies. Think of this as learning a new language—at first it seems foreign, but with practice, it becomes second nature.
What We'll Cover in This Article
By the end of this guide, you'll understand:
- What Typed Arrays are and why they exist
- The fundamental relationship between ArrayBuffer and Typed Arrays
- Why ArrayBuffer can't be accessed directly (and what that means)
- How Typed Arrays act as "views" into memory
- The concept of Uint8Array and working with bytes
- Three different ways to create Typed Arrays
- How to read and write binary data step-by-step
- The difference between views and copies
- Basic operations: accessing, modifying, and iterating
- Your first real-world example: reading file signatures
What We'll Explain Along the Way
These concepts will be explained with examples as we encounter them:
- Bytes and memory - What a byte is and how data is stored
- Views vs data - The critical distinction between looking at data and owning it
- Array-like behavior - Why Typed Arrays feel like regular arrays
- Type constraints - What happens when you try to store values outside the valid range
- Memory addressing - How computers locate specific bytes
- Binary representation - Seeing data as raw numbers
The Problem: ArrayBuffer Has No Door
Let's start by deeply understanding why we need Typed Arrays in the first place. This isn't just a technical detail—it's a fundamental design decision that shapes how JavaScript handles binary data.
Why ArrayBuffer is Locked
ArrayBuffer represents raw, unformatted memory. Think of it like an empty notebook with blank pages. The pages exist, but there's nothing written on them yet. More importantly, the notebook doesn't know what kind of information you want to write—numbers? Text? Drawings? Music notes?
// Create an ArrayBuffer with 8 bytes of storage
const buffer = new ArrayBuffer(8);
// Try to access it directly
console.log(buffer[0]); // undefined
console.log(buffer.length); // undefined - no length property!
// Try to write data
buffer[0] = 255; // Does nothing - silently fails
buffer[1] = 128; // Also does nothing
// Check what happened
console.log(buffer[0]); // Still undefined
console.log(buffer[1]); // Still undefined
// The only thing you can do is check its size
console.log(buffer.byteLength); // 8 - this works!
Why does ArrayBuffer work this way? There are three important reasons:
1. Type Ambiguity - The Notebook Problem
Imagine you're organizing a storage unit. You have boxes (ArrayBuffer), but you need to decide: will each box hold books, clothes, or dishes? The box itself doesn't know—it's just a container.
Similarly, ArrayBuffer doesn't know if its bytes should be interpreted as:
- Individual 8-bit numbers (0-255)
- Pairs of bytes representing 16-bit numbers (0-65,535)
- Groups of 4 bytes representing 32-bit numbers
- Decimal numbers (floating-point)
- Text characters
- Image pixels
- Audio samples
// The same 8 bytes can be interpreted many ways
const buffer = new ArrayBuffer(8);
// View as 8 separate bytes (8 × 1-byte values)
const view8 = new Uint8Array(buffer);
console.log(view8.length); // 8 elements
// View as 4 pairs of bytes (4 × 2-byte values)
const view16 = new Uint16Array(buffer);
console.log(view16.length); // 4 elements
// View as 2 groups of 4 bytes (2 × 4-byte values)
const view32 = new Uint32Array(buffer);
console.log(view32.length); // 2 elements
// Same storage, three different interpretations!
Visual representation:
2. Safety - The Locked Cabinet Principle
ArrayBuffer is intentionally "locked" to prevent accidents. If you could write data directly without specifying a type, you might:
- Accidentally overwrite data
- Misalign multi-byte values
- Mix incompatible data types
- Create corrupted binary data
// Imagine if this was allowed (it's not)
const buffer = new ArrayBuffer(4);
// What would this mean?
buffer[0] = 300; // Too large for a byte (max 255)
buffer[1] = 3.14; // A decimal number
buffer[2] = "A"; // A character
buffer[3] = true; // A boolean
// How would the computer store these different types?
// What size is each value?
// This is why direct access is not allowed!
3. Performance - The Express Lane Strategy
By requiring you to specify a type upfront (via a Typed Array), JavaScript can:
- Optimize memory access patterns
- Enable CPU-level optimizations
- Avoid runtime type checking
- Work directly with hardware
// With Typed Arrays, the computer knows exactly what to expect
const buffer = new ArrayBuffer(100);
const view = new Uint8Array(buffer);
// The CPU can optimize this loop because it knows:
// - Each element is exactly 1 byte
// - Values are always 0-255
// - No type conversions needed
for (let i = 0; i < view.length; i++) {
view[i] = i;
}
The Complete Storage Locker Analogy
Let's use a more detailed analogy to understand the ArrayBuffer + Typed Array relationship:
ArrayBuffer = Storage Locker
- It's just physical space
- You can check the size: "This locker is 8 cubic feet"
- But you can't put anything in or take anything out without a door
- You can't even look inside without access
Typed Array = The Door (and Organizational System)
- It's your way to access the locker
- It determines how you organize items inside
- Different doors = different organization systems
- You can even have multiple doors to the same locker!
Real-world parallel:
Think of a parking garage (ArrayBuffer) with spaces for 8 vehicles:
- Small vehicle door (Uint8Array): Fits 8 motorcycles (small values)
- Medium vehicle door (Uint16Array): Fits 4 cars (medium values)
- Large vehicle door (Uint32Array): Fits 2 trucks (large values)
Same garage space, different ways to use it!
Demonstrating the Problem
Let's see what happens when you try to work with ArrayBuffer alone:
// Step 1: Create storage
const buffer = new ArrayBuffer(8);
console.log("Created buffer:", buffer);
console.log("Buffer size:", buffer.byteLength, "bytes"); // 8
// Step 2: What properties does it have?
console.log("Properties:");
console.log(" buffer.length:", buffer.length); // undefined
console.log(" buffer[0]:", buffer[0]); // undefined
console.log(" buffer.byteLength:", buffer.byteLength); // 8 ✓
// Step 3: Try to use it like an array
console.log("\nTrying array operations:");
console.log(" buffer[0] = 100");
buffer[0] = 100;
console.log(" Result:", buffer[0]); // undefined - didn't work
console.log("\n buffer.push(200)");
try {
buffer.push(200);
} catch (error) {
console.log(" Error:", error.message); // push is not a function
}
// Step 4: What CAN you do?
console.log("\nWhat works:");
console.log(" Check size:", buffer.byteLength); // 8
console.log(" Create a copy:", buffer.slice(0, 4)); // ArrayBuffer(4)
console.log(" That's it!");
Output breakdown:
Created buffer: ArrayBuffer { byteLength: 8 }
Buffer size: 8 bytes
Properties:
buffer.length: undefined
buffer[0]: undefined
buffer.byteLength: 8
Trying array operations:
buffer[0] = 100
Result: undefined
buffer.push(200)
Error: buffer.push is not a function
What works:
Check size: 8
Create a copy: ArrayBuffer { byteLength: 4 }
That's it!
The frustration is real: You have storage, but no way to use it. This is exactly why Typed Arrays exist—they're the solution to this problem.
What is a Typed Array? The Complete Picture
Now that you understand the problem, let's explore the solution in detail.
The Basic Definition
A Typed Array is a special JavaScript object that provides a typed view into an ArrayBuffer. The word "view" is crucial here—it means:
- The Typed Array doesn't own the data
- It's a window into existing memory
- Multiple views can look at the same memory
- Changing data through one view affects all views
Key Characteristics:
// Create a Typed Array
const view = new Uint8Array(8);
// It looks like an array
console.log(view.length); // 8 (has length)
console.log(view[0]); // 0 (can access by index)
// It acts like an array
view[0] = 10;
view[1] = 20;
console.log(view); // Uint8Array(8) [10, 20, 0, 0, 0, 0, 0, 0]
// But it's typed - each element is a number from 0-255
view[2] = 300; // Too large, wraps to: 300 % 256 = 44
console.log(view[2]); // 44
// It has a fixed size
console.log(view.length); // 8 - cannot change
The View Concept: Looking vs Owning
This is one of the most important concepts to understand. Let's use multiple analogies to make it crystal clear.
Analogy 1: The Window
Imagine a room (ArrayBuffer) with several windows (Typed Arrays). Each window:
- Gives you a view into the room
- Lets you see and interact with what's inside
- Doesn't contain the furniture itself
- Shows the same furniture as all other windows
const buffer = new ArrayBuffer(4); // The room
const window1 = new Uint8Array(buffer); // First window
const window2 = new Uint8Array(buffer); // Second window
// Put furniture in through window1
window1[0] = 100;
// Look through window2 - you see the same furniture!
console.log(window2[0]); // 100
// Both windows show the same room
console.log(window1.buffer === window2.buffer); // true
Analogy 2: The Document with Multiple Readers
Think of a Google Doc (ArrayBuffer) that multiple people (Typed Arrays) are viewing:
- The document exists independently
- Each person sees the same content
- If one person edits, everyone sees the change
- The viewers don't "contain" the document
const document = new ArrayBuffer(10); // The Google Doc
const reader1 = new Uint8Array(document); // First reader
const reader2 = new Uint8Array(document); // Second reader
// Reader 1 makes an edit
reader1[0] = 65; // ASCII code for 'A'
// Reader 2 sees the change immediately
console.log(reader2[0]); // 65
// They're both reading the same document
console.log(reader1.buffer === reader2.buffer); // true
Visual Representation:
Understanding "Typed": What Does It Mean?
The word "typed" in "Typed Array" refers to the fact that each element must be a specific type of number. Unlike regular JavaScript arrays where you can mix any types:
// Regular JavaScript array - anything goes!
const regularArray = [
1, // number
"hello", // string
true, // boolean
{ x: 10 }, // object
[1, 2, 3], // array
null, // null
];
// Typed Array - only ONE type of number allowed
const typedArray = new Uint8Array(6);
typedArray[0] = 1; // ✓ Number
typedArray[1] = "hello"; // Converts to 0 (not what you expect!)
typedArray[2] = true; // Converts to 1
typedArray[3] = { x: 10 }; // Converts to 0
typedArray[4] = [1, 2]; // Converts to 0
typedArray[5] = null; // Converts to 0
console.log(typedArray); // Uint8Array(6) [1, 0, 1, 0, 0, 0]
// Only numbers are properly stored!
Why the restriction? Three key reasons:
1. Performance Optimization
When JavaScript knows every element is the same type, it can:
- Allocate memory efficiently
- Skip type checking on every access
- Use CPU instructions designed for that type
- Enable compiler optimizations
2. Memory Efficiency
Each type uses exactly the space it needs:
// Same 6 values stored in different typed arrays
const values = [10, 20, 30, 40, 50, 60];
// Regular array: ~48+ bytes (8 bytes per value + overhead)
const regular = [10, 20, 30, 40, 50, 60];
// Uint8Array: 6 bytes (1 byte per value)
const uint8 = new Uint8Array(values);
console.log(uint8.byteLength); // 6 bytes
// Uint16Array: 12 bytes (2 bytes per value)
const uint16 = new Uint16Array(values);
console.log(uint16.byteLength); // 12 bytes
// Uint32Array: 24 bytes (4 bytes per value)
const uint32 = new Uint32Array(values);
console.log(uint32.byteLength); // 24 bytes
Memory comparison chart:
Regular Array: ████████ (~48+ bytes)
Uint32Array: ████████████ (24 bytes)
Uint16Array: ██████ (12 bytes)
Uint8Array: ███ (6 bytes) ← 8× more efficient!
3. Binary Data Correctness
When working with files, networks, or hardware, data must be in exact formats:
// Reading a file's "magic number" (file type signature)
const fileData = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); // PNG signature
// Each byte must be exactly right
console.log(fileData[0]); // 137 (0x89 in decimal)
console.log(fileData[1]); // 80 (0x50 in decimal - 'P')
console.log(fileData[2]); // 78 (0x4E in decimal - 'N')
console.log(fileData[3]); // 71 (0x47 in decimal - 'G')
// If these aren't exact, file parsing fails!
The Type: Uint8Array (Our Primary Focus)
For this article, we'll focus on Uint8Array, the most fundamental and commonly used Typed Array. Understanding Uint8Array deeply will give you the foundation for all other Typed Array types.
What is Uint8Array?
- Uint = Unsigned Integer (no negative numbers)
- 8 = 8 bits = 1 byte
- Array = Array-like collection
Each element stores a number from 0 to 255 (that's 256 different values).
Why 0-255? Let's break it down:
8 bits means 2^8 possible combinations = 256 different values
In binary:
00000000 = 0 (all bits off)
00000001 = 1
00000010 = 2
00000011 = 3
...
11111110 = 254
11111111 = 255 (all bits on)
Visual representation of a byte:
Example: Storing the number 100
const view = new Uint8Array(1);
view[0] = 100;
// In binary: 01100100
// Breakdown:
// Bit 7 (128): 0 × 128 = 0
// Bit 6 (64): 1 × 64 = 64
// Bit 5 (32): 1 × 32 = 32
// Bit 4 (16): 0 × 16 = 0
// Bit 3 (8): 0 × 8 = 0
// Bit 2 (4): 0 × 4 = 0
// Bit 1 (2): 1 × 2 = 2
// Bit 0 (1): 0 × 1 = 0
// Total: 64 + 32 + 2 = 100 ✓
console.log(view[0]); // 100
console.log(view[0].toString(2)); // "1100100" (binary representation)
console.log(view[0].toString(16)); // "64" (hexadecimal representation)
What happens with values outside 0-255?
Uint8Array automatically wraps values using modulo arithmetic:
const view = new Uint8Array(5);
// Values in range work normally
view[0] = 0; // 0
view[1] = 128; // 128
view[2] = 255; // 255
// Values too large wrap around
view[3] = 256; // 256 % 256 = 0
view[4] = 300; // 300 % 256 = 44
console.log(view); // Uint8Array(5) [0, 128, 255, 0, 44]
// Think of it like a clock:
// - 13 o'clock wraps to 1 o'clock
// - 256 in Uint8 wraps to 0
// - 300 in Uint8 wraps to 44
Wrapping visualization:
Negative numbers wrap differently:
const view = new Uint8Array(3);
view[0] = -1; // Wraps to 255 (256 - 1)
view[1] = -10; // Wraps to 246 (256 - 10)
view[2] = -100; // Wraps to 156 (256 - 100)
console.log(view); // Uint8Array(3) [255, 246, 156]
// Why? Uint8 can only store 0-255
// Negative values wrap around from 255
Creating Your First Typed Array: Three Methods
There are three ways to create a Typed Array, each suited for different situations. We'll explore each method in detail with complete examples.
Method 1: Create from Size (Empty Array)
This is the most common method when you know how much data you need but will fill it later.
Syntax: new Uint8Array(length)
What happens:
- JavaScript creates a new ArrayBuffer (automatically)
- The buffer size = length × bytes per element
- All elements initialize to 0
- You get a view into this new buffer
// Create an array with 10 elements
const view = new Uint8Array(10);
// What do we have?
console.log("Length:", view.length); // 10 elements
console.log("Byte length:", view.byteLength); // 10 bytes (10 × 1)
console.log("Buffer:", view.buffer); // ArrayBuffer(10)
// All elements start at 0
console.log("Initial values:", view);
// Uint8Array(10) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
// Fill with data
for (let i = 0; i < view.length; i++) {
view[i] = i * 10;
}
console.log("After filling:", view);
// Uint8Array(10) [0, 10, 20, 30, 40, 50, 60, 70, 80, 90]
Step-by-step breakdown:
When to use this method:
// ✓ Good: You know the size upfront
const imagePixels = new Uint8Array(1920 * 1080 * 4); // RGBA pixels
// ✓ Good: Building data incrementally
const packetData = new Uint8Array(1024);
for (let i = 0; i < packetData.length; i++) {
packetData[i] = generateByte(i);
}
// ✓ Good: Pre-allocating for performance
const audioBuffer = new Uint8Array(44100); // 1 second at 44.1kHz
Common beginner mistakes:
// ❌ Wrong: Confusing length with byte size
const buffer = new ArrayBuffer(10);
const wrong = new Uint8Array(10); // Creates NEW buffer of 10 bytes!
// ✓ Right: Using existing buffer
const buffer2 = new ArrayBuffer(10);
const right = new Uint8Array(buffer2); // Uses existing buffer
// ❌ Wrong: Forgetting elements start at 0
const view = new Uint8Array(5);
// No need to initialize - already all zeros!
// ✓ Right: Only set non-zero values
const view2 = new Uint8Array(5);
view2[0] = 100; // Only set what you need
Method 2: Create from Existing ArrayBuffer (View)
This method creates a view into an existing ArrayBuffer. This is crucial when you receive binary data from files, networks, or other sources.
Syntax: new Uint8Array(buffer, byteOffset?, length?)
Parameters:
buffer
: The ArrayBuffer to viewbyteOffset
(optional): Starting position in bytes (default: 0)length
(optional): Number of elements (default: remaining bytes)
// Step 1: Create or receive an ArrayBuffer
const buffer = new ArrayBuffer(8); // 8 bytes of storage
// Step 2: Create a view
const view = new Uint8Array(buffer);
console.log("View length:", view.length); // 8 elements
console.log("View byte length:", view.byteLength); // 8 bytes
console.log("Same buffer?", view.buffer === buffer); // true ✓
// Step 3: Use the view
view[0] = 100;
view[1] = 200;
// The buffer now contains this data
console.log("View:", view); // Uint8Array(8) [100, 200, 0, 0, 0, 0, 0, 0]
Creating partial views:
const buffer = new ArrayBuffer(10); // 10 bytes
// View the entire buffer
const fullView = new Uint8Array(buffer);
console.log(fullView.length); // 10
// View starting at byte 2
const offsetView = new Uint8Array(buffer, 2);
console.log(offsetView.length); // 8 (10 - 2 = 8 remaining)
// View starting at byte 2, only 4 elements
const limitedView = new Uint8Array(buffer, 2, 4);
console.log(limitedView.length); // 4
// Visual representation of the buffer:
// Buffer: [0][1][2][3][4][5][6][7][8][9]
// fullView: 0 1 2 3 4 5 6 7 8 9
// offsetView: 0 1 2 3 4 5 6 7
// limitedView: 0 1 2 3
Memory layout diagram:
Practical example: Parsing a file header
// Simulate reading a file (first 16 bytes)
const fileData = new ArrayBuffer(16);
const bytes = new Uint8Array(fileData);
// Write simulated PNG file signature
bytes[0] = 0x89; // PNG magic number
bytes[1] = 0x50; // 'P'
bytes[2] = 0x4e; // 'N'
bytes[3] = 0x47; // 'G'
bytes[4] = 0x0d;
bytes[5] = 0x0a;
bytes[6] = 0x1a;
bytes[7] = 0x0a;
// Now parse different sections
const signature = new Uint8Array(fileData, 0, 8); // First 8 bytes
const chunkData = new Uint8Array(fileData, 8); // Remaining bytes
console.log("PNG Signature:", signature);
// Uint8Array(8) [137, 80, 78, 71, 13, 10, 26, 10]
console.log("Chunk Data:", chunkData);
// Uint8Array(8) [0, 0, 0, 0, 0, 0, 0, 0]
// Verify it's a PNG file
function isPNG(signature: Uint8Array): boolean {
return (
signature[0] === 0x89 &&
signature[1] === 0x50 &&
signature[2] === 0x4e &&
signature[3] === 0x47
);
}
console.log("Is PNG file?", isPNG(signature)); // true
When to use this method:
// ✓ Good: Working with received data
async function readFile(file: File) {
const buffer = await file.arrayBuffer();
const view = new Uint8Array(buffer); // View the file data
return view;
}
// ✓ Good: Parsing structured data
function parseHeader(buffer: ArrayBuffer) {
const header = new Uint8Array(buffer, 0, 32); // First 32 bytes
const body = new Uint8Array(buffer, 32); // Rest of data
return { header, body };
}
// ✓ Good: Sharing buffers between functions
function processData(buffer: ArrayBuffer) {
const view = new Uint8Array(buffer);
// Process the shared buffer
}
Important note about offsets:
const buffer = new ArrayBuffer(10);
// The offset is in BYTES, not elements
const view1 = new Uint8Array(buffer, 0); // Start at byte 0
const view2 = new Uint8Array(buffer, 5); // Start at byte 5
const view3 = new Uint8Array(buffer, 9); // Start at byte 9
console.log(view1.length); // 10 (all 10 bytes)
console.log(view2.length); // 5 (bytes 5-9)
console.log(view3.length); // 1 (only byte 9)
// Write through different views
view1[0] = 100; // Writes to byte 0
view2[0] = 200; // Writes to byte 5 (not byte 0!)
view3[0] = 50; // Writes to byte 9
// Memory layout:
// Byte index: [0] [1][2][3][4][5] [6][7][8][9]
// Values: [100][0][0][0][0][200][0][0][0][50]
// ↑ ↑ ↑
// view1[0] view2[0] view3[0]
Method 3: Create from Array or Values (With Data)
This method creates a new Typed Array and immediately fills it with data. It's the most convenient when you know your data upfront.
Syntax: new Uint8Array(arrayLike)
or Uint8Array.from()
// Method 3a: From a regular array
const regularArray = [10, 20, 30, 40, 50];
const view1 = new Uint8Array(regularArray);
console.log(view1); // Uint8Array(5) [10, 20, 30, 40, 50]
console.log(view1.length); // 5
console.log(view1.byteLength); // 5 bytes
// Method 3b: Using Uint8Array.from()
const view2 = Uint8Array.from([100, 150, 200, 250]);
console.log(view2); // Uint8Array(4) [100, 150, 200, 250]
// Method 3c: From another Typed Array (creates a copy)
const original = new Uint8Array([1, 2, 3]);
const copy = new Uint8Array(original);
// They are separate arrays with separate buffers
original[0] = 100;
console.log(original[0]); // 100
console.log(copy[0]); // 1 (unchanged - it's a copy!)
console.log(original.buffer === copy.buffer); // false
Step-by-step breakdown:
Using Array.from() with a mapping function:
// Create an array where each value is its index squared
const squares = Uint8Array.from({ length: 10 }, (_, i) => i * i);
console.log(squares);
// Uint8Array(10) [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
// Create an array of even numbers
const evens = Uint8Array.from({ length: 10 }, (_, i) => i * 2);
console.log(evens);
// Uint8Array(10) [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
// Generate a sine wave pattern
const sineWave = Uint8Array.from({ length: 20 }, (_, i) =>
Math.round(128 + 127 * Math.sin(i / 3))
);
console.log(sineWave);
// Uint8Array(20) [128, 156, 183, 206, 223, 234, ...]
When to use this method:
// ✓ Good: You have data ready
const rgbPixel = new Uint8Array([255, 128, 64]); // Red, Green, Blue
// ✓ Good: Converting from regular array
const numbers = [1, 2, 3, 4, 5];
const typed = new Uint8Array(numbers);
// ✓ Good: Creating test data
const testData = Uint8Array.from({ length: 100 }, (_, i) => i);
// ✓ Good: Duplicating for backup
const original = new Uint8Array([10, 20, 30]);
const backup = new Uint8Array(original); // Independent copy
Comparison: View vs Copy
const buffer = new ArrayBuffer(4);
const original = new Uint8Array(buffer);
original[0] = 10;
original[1] = 20;
// Method 2: Creates a VIEW (shares buffer)
const view = new Uint8Array(buffer);
view[0] = 100;
console.log(original[0]); // 100 (changed!)
console.log(view.buffer === original.buffer); // true (shared)
// Method 3: Creates a COPY (new buffer)
const copy = new Uint8Array(original);
copy[0] = 200;
console.log(original[0]); // 100 (unchanged)
console.log(copy.buffer === original.buffer); // false (separate)
Visual comparison:
Reading and Writing Data: Your First Operations
Now that you can create Typed Arrays, let's learn how to work with them. We'll explore every way to access and modify data.
Basic Access: Getting and Setting Values
Typed Arrays work just like regular arrays for basic access:
const view = new Uint8Array(5);
// Writing data (setting values)
view[0] = 10;
view[1] = 20;
view[2] = 30;
view[3] = 40;
view[4] = 50;
console.log(view); // Uint8Array(5) [10, 20, 30, 40, 50]
// Reading data (getting values)
console.log(view[0]); // 10
console.log(view[2]); // 30
console.log(view[4]); // 50
// Check the length
console.log(view.length); // 5
// Access beyond bounds returns undefined (like regular arrays)
console.log(view[10]); // undefined
console.log(view[-1]); // undefined
Step-by-step memory visualization:
Understanding Indices: Zero-Based Access
Like all JavaScript arrays, Typed Arrays use zero-based indexing:
const view = new Uint8Array(5);
// 0 1 2 3 4 ← Indices
view[0] = 65; // 'A' in ASCII
view[1] = 66; // 'B'
view[2] = 67; // 'C'
view[3] = 68; // 'D'
view[4] = 69; // 'E'
// First element: index 0
console.log("First:", view[0]); // 65
// Last element: index (length - 1)
console.log("Last:", view[view.length - 1]); // 69
// Middle element: index 2 (for array of length 5)
console.log("Middle:", view[2]); // 67
// Visual representation:
// Index: [0] [1] [2] [3] [4]
// Value: [65][66][67][68][69]
// ↑ ↑
// First Last
Common beginner mistake:
const view = new Uint8Array(10);
// ❌ Wrong: Trying to access "10th element"
console.log(view[10]); // undefined - no 10th element!
// ✓ Right: Last element is at index 9
console.log(view[9]); // 0
// Think of it like floors in a building:
// US: 1st floor, 2nd floor, 3rd floor
// Arrays: 0th element, 1st element, 2nd element
Iterating: Loops and Array Methods
You can iterate through Typed Arrays using familiar patterns:
Method 1: Traditional for loop
const view = new Uint8Array([10, 20, 30, 40, 50]);
// Classic for loop
for (let i = 0; i < view.length; i++) {
console.log(`Index ${i}: ${view[i]}`);
}
// Output:
// Index 0: 10
// Index 1: 20
// Index 2: 30
// Index 3: 40
// Index 4: 50
Method 2: for...of loop
const view = new Uint8Array([10, 20, 30, 40, 50]);
// Modern for...of loop (ES6)
for (const value of view) {
console.log(value);
}
// Output:
// 10
// 20
// 30
// 40
// 50
// With index using entries()
for (const [index, value] of view.entries()) {
console.log(`${index}: ${value}`);
}
// Output:
// 0: 10
// 1: 20
// 2: 30
// 3: 40
// 4: 50
Method 3: forEach() method
const view = new Uint8Array([10, 20, 30, 40, 50]);
// forEach with callback
view.forEach((value, index) => {
console.log(`Position ${index} has value ${value}`);
});
// Output:
// Position 0 has value 10
// Position 1 has value 20
// Position 2 has value 30
// Position 3 has value 40
// Position 4 has value 50
// Real-world example: Calculate sum
let sum = 0;
view.forEach((value) => {
sum += value;
});
console.log("Total:", sum); // 150
Method 4: Array methods (map, filter, reduce)
const view = new Uint8Array([10, 20, 30, 40, 50]);
// map: Transform each value
const doubled = view.map((x) => x * 2);
console.log(doubled); // Uint8Array(5) [20, 40, 60, 80, 100]
// filter: Select values that match condition
const large = view.filter((x) => x > 25);
console.log(large); // Uint8Array(3) [30, 40, 50]
// reduce: Combine all values
const sum = view.reduce((acc, val) => acc + val, 0);
console.log("Sum:", sum); // 150
// find: Get first matching value
const found = view.find((x) => x > 25);
console.log("Found:", found); // 30
// every: Check if all values match
const allPositive = view.every((x) => x > 0);
console.log("All positive?", allPositive); // true
// some: Check if any value matches
const hasLarge = view.some((x) => x > 100);
console.log("Has value > 100?", hasLarge); // false
Performance comparison:
const large = new Uint8Array(1000000); // 1 million elements
// Method 1: Traditional for loop (fastest)
console.time("for loop");
let sum1 = 0;
for (let i = 0; i < large.length; i++) {
sum1 += large[i];
}
console.timeEnd("for loop"); // ~1-2ms
// Method 2: forEach (slightly slower)
console.time("forEach");
let sum2 = 0;
large.forEach((v) => {
sum2 += v;
});
console.timeEnd("forEach"); // ~2-3ms
// Method 3: reduce (slowest but most expressive)
console.time("reduce");
const sum3 = large.reduce((a, v) => a + v, 0);
console.timeEnd("reduce"); // ~3-5ms
// For most use cases, readability > micro-optimizations
// Choose the method that makes your code clearest
Modifying Multiple Values: Fill and Set
Using fill(): Set all or some elements to the same value
const view = new Uint8Array(10);
// Fill entire array with 255
view.fill(255);
console.log(view);
// Uint8Array(10) [255, 255, 255, 255, 255, 255, 255, 255, 255, 255]
// Fill from index 2 to 5 (exclusive) with 100
view.fill(100, 2, 5);
console.log(view);
// Uint8Array(10) [255, 255, 100, 100, 100, 255, 255, 255, 255, 255]
// ↑----------↑
// Fill from index 7 to end with 0
view.fill(0, 7);
console.log(view);
// Uint8Array(10) [255, 255, 100, 100, 100, 255, 255, 0, 0, 0]
// ↑----↑
Using set(): Copy values from another array
const target = new Uint8Array(10);
const source = new Uint8Array([10, 20, 30, 40]);
// Copy source to target starting at index 0
target.set(source);
console.log(target);
// Uint8Array(10) [10, 20, 30, 40, 0, 0, 0, 0, 0, 0]
// Copy source to target starting at index 5
target.set(source, 5);
console.log(target);
// Uint8Array(10) [10, 20, 30, 40, 0, 10, 20, 30, 40, 0]
// ↑-----------↑
// Copy from regular array
target.set([100, 101, 102], 0);
console.log(target);
// Uint8Array(10) [100, 101, 102, 40, 0, 10, 20, 30, 40, 0]
Visual representation of set():
Real-world example: Building a data packet
// Build a network packet with header and data
const packet = new Uint8Array(64); // 64-byte packet
// Header (first 4 bytes)
const header = new Uint8Array([
0x01, // Version
0x02, // Type
0x00, // Flags
0x3c, // Length (60 bytes)
]);
// Copy header to packet
packet.set(header, 0);
// Payload data (next 60 bytes)
const payload = new Uint8Array(60);
payload.fill(65); // Fill with 'A' (ASCII 65)
// Copy payload after header
packet.set(payload, 4);
console.log("Packet structure:");
console.log("Header:", packet.slice(0, 4));
// Uint8Array(4) [1, 2, 0, 60]
console.log("Payload sample:", packet.slice(4, 10));
// Uint8Array(6) [65, 65, 65, 65, 65, 65]
console.log("Total size:", packet.byteLength, "bytes"); // 64
Value Constraints: What Happens with Invalid Values?
Remember, Uint8Array can only store numbers from 0 to 255. What happens when you try to store something else?
Numbers outside range: Wrapping
const view = new Uint8Array(6);
// Valid values work normally
view[0] = 0; // Min value
view[1] = 255; // Max value
view[2] = 128; // Middle value
// Values too large wrap around
view[3] = 256; // Wraps to: 256 % 256 = 0
view[4] = 300; // Wraps to: 300 % 256 = 44
view[5] = 512; // Wraps to: 512 % 256 = 0
console.log(view);
// Uint8Array(6) [0, 255, 128, 0, 44, 0]
// Think of it like a clock:
// 12 hours -> wraps to 0
// 13 hours -> wraps to 1
// 256 in Uint8 -> wraps to 0
// 300 in Uint8 -> wraps to 44
Negative numbers: Wrap from top
const view = new Uint8Array(5);
view[0] = -1; // Wraps to: 256 - 1 = 255
view[1] = -10; // Wraps to: 256 - 10 = 246
view[2] = -100; // Wraps to: 256 - 100 = 156
view[3] = -256; // Wraps to: 256 - 256 = 0
view[4] = -300; // Wraps to: 256 - (300 % 256) = 256 - 44 = 212
console.log(view);
// Uint8Array(5) [255, 246, 156, 0, 212]
Wrapping visualization:
Non-numeric values: Convert to number
const view = new Uint8Array(8);
view[0] = "100"; // String → Number: 100
view[1] = "hello"; // Invalid string → NaN → 0
view[2] = true; // Boolean true → 1
view[3] = false; // Boolean false → 0
view[4] = null; // null → 0
view[5] = undefined; // undefined → 0
view[6] = { x: 10 }; // Object → NaN → 0
view[7] = [50]; // Array [50] → 50
console.log(view);
// Uint8Array(8) [100, 0, 1, 0, 0, 0, 0, 50]
// Lesson: Only use numbers with Typed Arrays!
// Anything else converts in unexpected ways
Decimal numbers: Truncate to integer
const view = new Uint8Array(5);
view[0] = 10.1; // Becomes 10
view[1] = 10.9; // Becomes 10 (not rounded!)
view[2] = 99.999; // Becomes 99
view[3] = 3.14; // Becomes 3
view[4] = 2.71; // Becomes 2
console.log(view);
// Uint8Array(5) [10, 10, 99, 3, 2]
// Uint8Array TRUNCATES (cuts off) decimals
// It doesn't round them!
// If you need to round first:
view[0] = Math.round(10.9); // 11
console.log(view[0]); // 11
Your First Real-World Example: Reading File Signatures
Now let's apply everything you've learned to solve a real problem: identifying file types by reading their "magic numbers" (unique byte sequences at the start of files).
Understanding File Signatures
Every file type has a unique signature—a specific sequence of bytes at the beginning of the file that identifies what type of file it is.
Common file signatures:
File Type | Magic Number (Hex) | Magic Number (Decimal) | First Bytes |
---|---|---|---|
PNG | 89 50 4E 47 | 137, 80, 78, 71 | �PNG |
JPEG | FF D8 FF | 255, 216, 255 | ÿØÿ |
GIF | 47 49 46 38 | 71, 73, 70, 56 | GIF8 |
25 50 44 46 | 37, 80, 68, 70 | ||
ZIP | 50 4B 03 04 | 80, 75, 3, 4 | PK.. |
Why this matters:
File extensions (.jpg
, .png
) can be changed easily—anyone can rename photo.png
to photo.txt
. But the magic number inside the file doesn't change. This is how browsers and operating systems truly identify files.
Complete Example: File Type Detector
/**
* Identifies a file type by reading its magic number
*/
class FileTypeDetector {
/**
* Check if the file is a PNG image
*/
private static isPNG(bytes: Uint8Array): boolean {
// PNG signature: 137, 80, 78, 71, 13, 10, 26, 10
return (
bytes.length >= 8 &&
bytes[0] === 137 && // 0x89
bytes[1] === 80 && // 0x50 'P'
bytes[2] === 78 && // 0x4E 'N'
bytes[3] === 71 && // 0x47 'G'
bytes[4] === 13 && // 0x0D
bytes[5] === 10 && // 0x0A
bytes[6] === 26 && // 0x1A
bytes[7] === 10
); // 0x0A
}
/**
* Check if the file is a JPEG image
*/
private static isJPEG(bytes: Uint8Array): boolean {
// JPEG signature: 255, 216, 255
return (
bytes.length >= 3 &&
bytes[0] === 255 && // 0xFF
bytes[1] === 216 && // 0xD8
bytes[2] === 255
); // 0xFF
}
/**
* Check if the file is a GIF image
*/
private static isGIF(bytes: Uint8Array): boolean {
// GIF signature: "GIF8" = 71, 73, 70, 56
return (
bytes.length >= 4 &&
bytes[0] === 71 && // 'G'
bytes[1] === 73 && // 'I'
bytes[2] === 70 && // 'F'
bytes[3] === 56
); // '8'
}
/**
* Check if the file is a PDF document
*/
private static isPDF(bytes: Uint8Array): boolean {
// PDF signature: "%PDF" = 37, 80, 68, 70
return (
bytes.length >= 4 &&
bytes[0] === 37 && // '%'
bytes[1] === 80 && // 'P'
bytes[2] === 68 && // 'D'
bytes[3] === 70
); // 'F'
}
/**
* Check if the file is a ZIP archive
*/
private static isZIP(bytes: Uint8Array): boolean {
// ZIP signature: "PK" = 80, 75, 3, 4
return (
bytes.length >= 4 &&
bytes[0] === 80 && // 'P'
bytes[1] === 75 && // 'K'
bytes[2] === 3 &&
bytes[3] === 4
);
}
/**
* Detect file type from byte array
*/
static detect(bytes: Uint8Array): string {
if (this.isPNG(bytes)) return "PNG image";
if (this.isJPEG(bytes)) return "JPEG image";
if (this.isGIF(bytes)) return "GIF image";
if (this.isPDF(bytes)) return "PDF document";
if (this.isZIP(bytes)) return "ZIP archive";
return "Unknown file type";
}
/**
* Detect file type from a File object
*/
static async detectFromFile(file: File): Promise<string> {
// Read first 16 bytes (enough for most signatures)
const blob = file.slice(0, 16);
const buffer = await blob.arrayBuffer();
const bytes = new Uint8Array(buffer);
return this.detect(bytes);
}
/**
* Display magic number in human-readable format
*/
static displaySignature(bytes: Uint8Array, length: number = 8): string {
const hex = Array.from(bytes.slice(0, length))
.map((b) => b.toString(16).padStart(2, "0").toUpperCase())
.join(" ");
const decimal = Array.from(bytes.slice(0, length)).join(", ");
const ascii = Array.from(bytes.slice(0, length))
.map((b) => (b >= 32 && b <= 126 ? String.fromCharCode(b) : "."))
.join("");
return `Hex: ${hex}\nDecimal: ${decimal}\nASCII: ${ascii}`;
}
}
// Usage example with HTML file input
function setupFileDetector() {
const fileInput =
document.querySelector<HTMLInputElement>('input[type="file"]');
if (!fileInput) {
console.log("Add this HTML to your page:");
console.log('<input type="file" id="fileInput">');
return;
}
fileInput.addEventListener("change", async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
console.log("=== File Analysis ===");
console.log("File name:", file.name);
console.log("File size:", file.size, "bytes");
console.log("Extension:", file.name.split(".").pop());
// Detect actual file type
const detectedType = await FileTypeDetector.detectFromFile(file);
console.log("Detected type:", detectedType);
// Show magic number
const blob = file.slice(0, 16);
const buffer = await blob.arrayBuffer();
const bytes = new Uint8Array(buffer);
console.log("\nMagic Number:");
console.log(FileTypeDetector.displaySignature(bytes));
});
}
// Call this to set up the detector
// setupFileDetector();
Step-by-step breakdown of the detection process:
Example output:
// When you select a PNG file:
=== File Analysis ===
File name: photo.png
File size: 2048576 bytes
Extension: png
Detected type: PNG image
Magic Number:
Hex: 89 50 4E 47 0D 0A 1A 0A
Decimal: 137, 80, 78, 71, 13, 10, 26, 10
ASCII: .PNG....
// When you select a JPEG file:
=== File Analysis ===
File name: image.jpg
File size: 1024000 bytes
Extension: jpg
Detected type: JPEG image
Magic Number:
Hex: FF D8 FF E0 00 10 4A 46
Decimal: 255, 216, 255, 224, 0, 16, 74, 70
ASCII: ....JF
Understanding the Magic: How It Works
Let's break down each part of the file detector:
Part 1: Reading the file
// Step 1: Get the first 16 bytes of the file
const blob = file.slice(0, 16);
// Why 16? Most signatures are 4-8 bytes, 16 is safely enough
// Step 2: Convert to ArrayBuffer
const buffer = await blob.arrayBuffer();
// Now we have raw binary data
// Step 3: Create a Uint8Array view
const bytes = new Uint8Array(buffer);
// Now we can read individual bytes!
Part 2: Checking signatures
// PNG check explained
private static isPNG(bytes: Uint8Array): boolean {
return bytes.length >= 8 && // Make sure we have enough bytes
bytes[0] === 137 && // First byte must be 137
bytes[1] === 80 && // Second byte must be 80 ('P')
bytes[2] === 78 && // Third byte must be 78 ('N')
bytes[3] === 71 && // Fourth byte must be 71 ('G')
// ... and so on
}
// Think of it like a password:
// File: "What's the password?"
// PNG: "137, 80, 78, 71, 13, 10, 26, 10"
// File: "Correct! I'm a PNG."
Part 3: Displaying the signature
// Convert bytes to hexadecimal (how developers read binary)
const hex = bytes.map((b) => b.toString(16).padStart(2, "0"));
// [137, 80, 78, 71] → ["89", "50", "4E", "47"]
// Show as decimal (human-readable numbers)
const decimal = Array.from(bytes).join(", ");
// [137, 80, 78, 71] → "137, 80, 78, 71"
// Show as ASCII (readable characters)
const ascii = bytes.map((b) =>
b >= 32 && b <= 126 ? String.fromCharCode(b) : "."
);
// [137, 80, 78, 71] → ['.', 'P', 'N', 'G']
Visual representation:
Practical Extension: Validation
Here's how you might use this in a real application:
/**
* Validate uploaded image files
*/
async function validateImageUpload(file: File): Promise<boolean> {
// Check file extension (can be faked)
const extension = file.name.split(".").pop()?.toLowerCase();
console.log("Extension says:", extension);
// Check actual file content (cannot be faked)
const type = await FileTypeDetector.detectFromFile(file);
console.log("Content says:", type);
// Accept only PNG, JPEG, and GIF
const validTypes = ["PNG image", "JPEG image", "GIF image"];
const isValid = validTypes.includes(type);
if (!isValid) {
console.error("Invalid file type!");
console.error("Expected: PNG, JPEG, or GIF");
console.error("Got:", type);
}
return isValid;
}
// Usage in a form handler
async function handleImageUpload(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
const isValid = await validateImageUpload(file);
if (isValid) {
console.log("✓ File is valid! Proceeding with upload...");
// Upload the file
} else {
console.log("✗ File is invalid! Rejecting upload.");
input.value = ""; // Clear the input
alert("Please upload a valid image file (PNG, JPEG, or GIF)");
}
}
Why this approach is better than checking extensions:
// ❌ Weak validation - can be bypassed
function weakValidation(file: File): boolean {
const extension = file.name.split(".").pop();
return extension === "png" || extension === "jpg" || extension === "gif";
// Problem: User can rename "virus.exe" to "photo.png"
}
// ✅ Strong validation - checks actual content
async function strongValidation(file: File): Promise<boolean> {
const type = await FileTypeDetector.detectFromFile(file);
return ["PNG image", "JPEG image", "GIF image"].includes(type);
// Even if renamed, "virus.exe" won't have image magic numbers
}
Understanding Views vs Copies: A Critical Distinction
One of the most important concepts with Typed Arrays is understanding when you're creating a view (looking at existing data) versus a copy (creating new independent data).
Views: Shared Memory
When you create a view, you're looking at the same memory through a different lens. Changes through one view affect all views.
// Create a buffer
const buffer = new ArrayBuffer(8);
// Create two views of the SAME buffer
const view1 = new Uint8Array(buffer);
const view2 = new Uint8Array(buffer);
// Write through view1
view1[0] = 100;
view1[1] = 200;
// Read through view2 - sees the same changes!
console.log(view2[0]); // 100
console.log(view2[1]); // 200
// Verify they share the buffer
console.log(view1.buffer === view2.buffer); // true ✓
// Memory diagram:
// Buffer: [100][200][0][0][0][0][0][0]
// ↑ ↑
// view1 & view2 both see this
Real-world analogy: Security cameras
Think of the ArrayBuffer as a room, and views as security cameras:
- Multiple cameras (views) watch the same room (buffer)
- If something moves in the room, all cameras see it
- The cameras don't contain the room—they just look at it
Copies: Independent Memory
When you create a copy, you're creating entirely new memory with duplicated data. Changes to one don't affect the other.
// Create original
const original = new Uint8Array([10, 20, 30]);
// Create a COPY
const copy = new Uint8Array(original);
// Modify the original
original[0] = 100;
// Copy is unaffected
console.log(original[0]); // 100 (changed)
console.log(copy[0]); // 10 (unchanged)
// Verify they have different buffers
console.log(original.buffer === copy.buffer); // false ✓
// Memory diagram:
// Original buffer: [100][20][30]
// Copy buffer: [10][20][30]
// ↑
// Different memory!
Real-world analogy: Photographs
Think of a copy as taking a photograph:
- The original room and the photograph are separate
- Changes to the room don't affect the photograph
- The photograph is a snapshot frozen in time
How to Create Each
Creating a view (Method 2):
const buffer = new ArrayBuffer(10);
// These all create VIEWS of the same buffer
const view1 = new Uint8Array(buffer); // Full buffer
const view2 = new Uint8Array(buffer, 0, 5); // First 5 bytes
const view3 = new Uint8Array(buffer, 5); // Last 5 bytes
// All share the same buffer
console.log(view1.buffer === buffer); // true
console.log(view2.buffer === buffer); // true
console.log(view3.buffer === buffer); // true
// Changes through any view affect all
view1[0] = 100;
view2[0] = 200; // Overwrites view1[0]
console.log(view1[0]); // 200
Creating a copy (Method 3):
const original = new Uint8Array([10, 20, 30]);
// These all create COPIES with new buffers
const copy1 = new Uint8Array(original); // Constructor
const copy2 = Uint8Array.from(original); // .from()
const copy3 = original.slice(); // .slice()
const copy4 = new Uint8Array([...original]); // Spread operator
// All have different buffers
console.log(copy1.buffer === original.buffer); // false
console.log(copy2.buffer === original.buffer); // false
console.log(copy3.buffer === original.buffer); // false
console.log(copy4.buffer === original.buffer); // false
// Changes to original don't affect copies
original[0] = 100;
console.log(original[0]); // 100
console.log(copy1[0]); // 10 (unchanged)
console.log(copy2[0]); // 10 (unchanged)
When to Use Each
Use views when:
// ✓ You want to parse different parts of the same data
const fileData = new ArrayBuffer(1024);
const header = new Uint8Array(fileData, 0, 64); // First 64 bytes
const body = new Uint8Array(fileData, 64); // Rest of file
// ✓ You want multiple interpretations of the same data
const buffer = new ArrayBuffer(8);
const bytes = new Uint8Array(buffer); // View as bytes
const uint16s = new Uint16Array(buffer); // View as 16-bit values
// ✓ You want to modify the original data
function processInPlace(buffer: ArrayBuffer) {
const view = new Uint8Array(buffer);
view[0] = view[0] + 1; // Modifies original buffer
}
Use copies when:
// ✓ You want to preserve the original
const original = new Uint8Array([1, 2, 3]);
const backup = new Uint8Array(original);
original[0] = 100; // backup still has [1, 2, 3]
// ✓ You want to modify without affecting the source
const source = new Uint8Array([10, 20, 30]);
const modified = new Uint8Array(source);
modified[0] = 100; // source still has [10, 20, 30]
// ✓ You want to pass data to another function safely
function processSafely(data: Uint8Array) {
const copy = new Uint8Array(data);
// Work with copy, original untouched
}
Common Mistake: Accidental Sharing
// ❌ MISTAKE: Thinking you made a copy
const buffer = new ArrayBuffer(10);
const original = new Uint8Array(buffer);
const notACopy = new Uint8Array(buffer); // Still a view!
original[0] = 100;
console.log(notACopy[0]); // 100 - oops, it changed!
// ✅ CORRECT: Actually making a copy
const buffer2 = new ArrayBuffer(10);
const original2 = new Uint8Array(buffer2);
const actualCopy = new Uint8Array(original2); // New buffer created
original2[0] = 100;
console.log(actualCopy[0]); // 0 - unchanged, as expected
Debugging tip:
// Check if two arrays share a buffer
function sharesBuffer(arr1: Uint8Array, arr2: Uint8Array): boolean {
return arr1.buffer === arr2.buffer;
}
const buffer = new ArrayBuffer(10);
const view1 = new Uint8Array(buffer);
const view2 = new Uint8Array(buffer);
const copy = new Uint8Array(view1);
console.log(sharesBuffer(view1, view2)); // true (both views)
console.log(sharesBuffer(view1, copy)); // false (copy has new buffer)
Array-Like Behavior: What Works and What Doesn't
Typed Arrays look and feel like regular JavaScript arrays, but there are important differences. Let's explore what's supported and what's not.
What Works: Supported Operations
Accessing and iterating:
const arr = new Uint8Array([10, 20, 30, 40, 50]);
// ✓ Index access
console.log(arr[0]); // 10
console.log(arr[4]); // 50
// ✓ Length property
console.log(arr.length); // 5
// ✓ for loop
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
// ✓ for...of loop
for (const value of arr) {
console.log(value);
}
// ✓ forEach
arr.forEach((value, index) => {
console.log(index, value);
});
// ✓ entries, keys, values
for (const [index, value] of arr.entries()) {
console.log(index, value);
}
Transformation methods:
const arr = new Uint8Array([10, 20, 30, 40, 50]);
// ✓ map - creates new array
const doubled = arr.map((x) => x * 2);
console.log(doubled); // Uint8Array(5) [20, 40, 60, 80, 100]
// ✓ filter - creates new array
const large = arr.filter((x) => x > 25);
console.log(large); // Uint8Array(3) [30, 40, 50]
// ✓ reduce - aggregates to single value
const sum = arr.reduce((acc, val) => acc + val, 0);
console.log(sum); // 150
// ✓ find - returns first match
const found = arr.find((x) => x > 25);
console.log(found); // 30
// ✓ findIndex - returns index of first match
const index = arr.findIndex((x) => x > 25);
console.log(index); // 2
// ✓ some - checks if any match
const hasLarge = arr.some((x) => x > 40);
console.log(hasLarge); // true
// ✓ every - checks if all match
const allPositive = arr.every((x) => x > 0);
console.log(allPositive); // true
// ✓ indexOf - finds element position
const position = arr.indexOf(30);
console.log(position); // 2
// ✓ includes - checks if element exists
const has30 = arr.includes(30);
console.log(has30); // true
Slicing and extracting:
const arr = new Uint8Array([10, 20, 30, 40, 50]);
// ✓ slice - creates a copy of a portion
const portion = arr.slice(1, 4);
console.log(portion); // Uint8Array(3) [20, 30, 40]
// ✓ subarray - creates a view of a portion
const view = arr.subarray(1, 4);
console.log(view); // Uint8Array(3) [20, 30, 40]
// Difference: slice copies, subarray views
arr[2] = 100;
console.log(portion[1]); // 30 (slice - unchanged)
console.log(view[1]); // 100 (subarray - changed!)
In-place modifications:
const arr = new Uint8Array([10, 20, 30, 40, 50]);
// ✓ fill - set all or some elements
arr.fill(0);
console.log(arr); // Uint8Array(5) [0, 0, 0, 0, 0]
arr.fill(99, 2, 4);
console.log(arr); // Uint8Array(5) [0, 0, 99, 99, 0]
// ✓ set - copy values from another array
arr.set([10, 20, 30], 0);
console.log(arr); // Uint8Array(5) [10, 20, 30, 99, 0]
// ✓ copyWithin - copy within same array
arr.copyWithin(2, 0, 2);
console.log(arr); // Uint8Array(5) [10, 20, 10, 20, 0]
// ✓ reverse - reverse in place
arr.reverse();
console.log(arr); // Uint8Array(5) [0, 20, 10, 20, 10]
// ✓ sort - sort in place
arr.sort();
console.log(arr); // Uint8Array(5) [0, 10, 10, 20, 20]
What Doesn't Work: Unsupported Operations
These methods don't exist on Typed Arrays because they would change the array's size:
const arr = new Uint8Array([10, 20, 30]);
// ❌ push - Error: push is not a function
// arr.push(40);
// ❌ pop - Error: pop is not a function
// arr.pop();
// ❌ shift - Error: shift is not a function
// arr.shift();
// ❌ unshift - Error: unshift is not a function
// arr.unshift(5);
// ❌ splice - Error: splice is not a function
// arr.splice(1, 1);
// ❌ concat - Error: concat is not a function
// arr.concat([40, 50]);
// WHY? Typed Arrays have FIXED size
// They cannot grow or shrink after creation
Why the restriction?
// Typed Arrays are backed by ArrayBuffer
const buffer = new ArrayBuffer(5); // Exactly 5 bytes allocated
const arr = new Uint8Array(buffer);
// If arr.push() worked, where would the 6th element go?
// The buffer is only 5 bytes!
// JavaScript would need to:
// 1. Allocate a new larger buffer
// 2. Copy all existing data
// 3. Add the new element
// This is expensive and defeats the performance benefits
// Regular arrays can do this because they're dynamically sized
// Typed Arrays trade this flexibility for speed and efficiency
Workarounds:
const arr = new Uint8Array([10, 20, 30]);
// ✓ To "add" an element, create a new larger array
const extended = new Uint8Array(arr.length + 1);
extended.set(arr); // Copy original data
extended[arr.length] = 40; // Add new element
console.log(extended); // Uint8Array(4) [10, 20, 30, 40]
// ✓ To "remove" an element, create a new smaller array
const shortened = new Uint8Array(arr.length - 1);
shortened.set(arr.subarray(0, arr.length - 1));
console.log(shortened); // Uint8Array(2) [10, 20]
// ✓ To "insert", create new array and copy in parts
function insertAt(arr: Uint8Array, index: number, value: number): Uint8Array {
const result = new Uint8Array(arr.length + 1);
result.set(arr.subarray(0, index), 0); // Before insertion
result[index] = value; // New value
result.set(arr.subarray(index), index + 1); // After insertion
return result;
}
const inserted = insertAt(arr, 1, 15);
console.log(inserted); // Uint8Array(4) [10, 15, 20, 30]
Comparison Table
Operation | Regular Array | Typed Array | Notes |
---|---|---|---|
arr[i] | ✅ Yes | ✅ Yes | Index access |
arr.length | ✅ Yes | ✅ Yes | Get size |
arr.push() | ✅ Yes | ❌ No | Would change size |
arr.pop() | ✅ Yes | ❌ No | Would change size |
arr.map() | ✅ Yes | ✅ Yes | Creates new array |
arr.filter() | ✅ Yes | ✅ Yes | Creates new array |
arr.reduce() | ✅ Yes | ✅ Yes | Aggregates values |
arr.slice() | ✅ Yes | ✅ Yes | Creates copy |
arr.splice() | ✅ Yes | ❌ No | Would change size |
arr.concat() | ✅ Yes | ❌ No | Would change size |
arr.fill() | ✅ Yes | ✅ Yes | Modifies in place |
arr.sort() | ✅ Yes | ✅ Yes | Modifies in place |
Common Beginner Mistakes and How to Avoid Them
Let's go through the most common mistakes beginners make with Typed Arrays and learn how to avoid them.
Mistake 1: Confusing Length with ByteLength
// ❌ MISTAKE
const arr = new Uint8Array(10);
console.log("Size:", arr.byteLength); // Correct: 10 bytes
// But then using it wrong:
const buffer = new ArrayBuffer(arr.byteLength);
const view = new Uint8Array(buffer.byteLength); // WRONG!
// This creates a Uint8Array with byteLength elements,
// not a view of the buffer!
// ✅ CORRECT
const arr2 = new Uint8Array(10);
const buffer2 = new ArrayBuffer(arr2.byteLength);
const view2 = new Uint8Array(buffer2); // Pass the buffer, not its size
Understanding the difference:
const arr = new Uint8Array(10);
// length: number of ELEMENTS
console.log(arr.length); // 10 elements
// byteLength: number of BYTES
console.log(arr.byteLength); // 10 bytes (10 elements × 1 byte each)
// They happen to be the same for Uint8Array because each element is 1 byte
// But for other types, they differ:
const arr16 = new Uint16Array(10);
console.log(arr16.length); // 10 elements
console.log(arr16.byteLength); // 20 bytes (10 elements × 2 bytes each)
const arr32 = new Uint32Array(10);
console.log(arr32.length); // 10 elements
console.log(arr32.byteLength); // 40 bytes (10 elements × 4 bytes each)
Memory aid:
.length = How many items can I store?
.byteLength = How much memory does this use?
Mistake 2: Forgetting Values Wrap Around
// ❌ MISTAKE: Expecting values to cap at 255
const arr = new Uint8Array(3);
arr[0] = 300; // Expects 255, gets 44
arr[1] = 500; // Expects 255, gets 244
arr[2] = -10; // Expects 0, gets 246
console.log(arr); // Uint8Array(3) [44, 244, 246] - surprising!
// ✅ CORRECT: Clamp values manually
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
arr[0] = clamp(300, 0, 255); // 255
arr[1] = clamp(500, 0, 255); // 255
arr[2] = clamp(-10, 0, 255); // 0
console.log(arr); // Uint8Array(3) [255, 255, 0] - as expected!
// Or use Uint8ClampedArray for automatic clamping
const clamped = new Uint8ClampedArray(3);
clamped[0] = 300; // Automatically becomes 255
clamped[1] = 500; // Automatically becomes 255
clamped[2] = -10; // Automatically becomes 0
console.log(clamped); // Uint8ClampedArray(3) [255, 255, 0]
Mistake 3: Trying to Resize Arrays
// ❌ MISTAKE: Trying to change length
const arr = new Uint8Array(5);
arr.length = 10; // Does nothing (length is read-only)
console.log(arr.length); // Still 5
// ❌ MISTAKE: Trying to add elements
arr.push(60); // Error: push is not a function
// ✅ CORRECT: Create a new larger array
function resize(arr: Uint8Array, newSize: number): Uint8Array {
const newArr = new Uint8Array(newSize);
// Copy as much as will fit
const copyLength = Math.min(arr.length, newSize);
newArr.set(arr.subarray(0, copyLength));
return newArr;
}
const original = new Uint8Array([10, 20, 30]);
const larger = resize(original, 5);
console.log(larger); // Uint8Array(5) [10, 20, 30, 0, 0]
const smaller = resize(original, 2);
console.log(smaller); // Uint8Array(2) [10, 20]
Mistake 4: Confusing Views and Copies
// ❌ MISTAKE: Thinking this creates a copy
const buffer = new ArrayBuffer(10);
const original = new Uint8Array(buffer);
const notACopy = new Uint8Array(buffer); // Still shares buffer!
original[0] = 100;
console.log(notACopy[0]); // 100 - oops!
// ✅ CORRECT: Actually create a copy
const actualCopy = new Uint8Array(original); // New buffer
original[0] = 200;
console.log(actualCopy[0]); // Still 100 - independent!
// ✅ Or use slice
const copy2 = original.slice();
console.log(copy2.buffer === original.buffer); // false - different buffers
Mistake 5: Ignoring Type Conversion
// ❌ MISTAKE: Storing non-numbers without checking
const arr = new Uint8Array(5);
arr[0] = "hello"; // Becomes 0 (unexpected!)
arr[1] = true; // Becomes 1 (might be ok)
arr[2] = { x: 10 }; // Becomes 0 (unexpected!)
arr[3] = [1, 2]; // Becomes 0 (unexpected!)
arr[4] = undefined; // Becomes 0 (unexpected!)
console.log(arr); // Uint8Array(5) [0, 1, 0, 0, 0] - surprising!
// ✅ CORRECT: Always use numbers
const arr2 = new Uint8Array(5);
arr2[0] = 72; // 'H' in ASCII
arr2[1] = 101; // 'e'
arr2[2] = 108; // 'l'
arr2[3] = 108; // 'l'
arr2[4] = 111; // 'o'
console.log(arr2); // Uint8Array(5) [72, 101, 108, 108, 111]
// Convert to string
const text = String.fromCharCode(...arr2);
console.log(text); // "Hello"
Mistake 6: Off-by-One Errors with Offset Views
// ❌ MISTAKE: Confusing byte offset with element index
const buffer = new ArrayBuffer(10);
const view = new Uint8Array(buffer, 2, 4); // Start at BYTE 2, length 4
// This view sees bytes 2, 3, 4, 5 of the buffer
// But its indices are 0, 1, 2, 3
view[0] = 100; // Writes to BYTE 2 of buffer (not byte 0!)
view[1] = 200; // Writes to BYTE 3 of buffer
console.log("View index 0 is at buffer byte 2");
// ✅ CORRECT: Remember the offset
const fullView = new Uint8Array(buffer);
console.log(fullView[2]); // 100 (same as view[0])
console.log(fullView[3]); // 200 (same as view[1])
// Visualize the relationship:
// Buffer bytes: [0][1][2][3][4][5][6][7][8][9]
// View indices: [0][1][2][3]
// ↑
// view[0] = buffer byte 2 (offset of 2)
Summary: Key Takeaways
Let's recap everything you've learned about Typed Arrays fundamentals:
The Core Concepts
Essential truths about Typed Arrays:
✓ They're views, not containers - Typed Arrays don't own data, they provide typed access to ArrayBuffer
✓ They have fixed sizes - Cannot grow or shrink after creation (no push/pop/splice)
✓ They enforce type constraints - Values outside range wrap around (0-255 for Uint8Array)
✓ They're array-like - Support most array methods (map, filter, forEach, etc.)
✓ They share buffers - Multiple views can access the same memory
✓ They're performant - Faster and more memory-efficient than regular arrays for numeric data
The Three Creation Methods
// Method 1: From size (creates new buffer automatically)
const arr1 = new Uint8Array(10);
// Use when: You know the size, will fill data later
// Method 2: From buffer (creates view of existing buffer)
const buffer = new ArrayBuffer(10);
const arr2 = new Uint8Array(buffer);
// Use when: You have binary data to read (files, network)
// Method 3: From values (creates new buffer with data)
const arr3 = new Uint8Array([10, 20, 30]);
// Use when: You have data ready to store
Critical Distinctions
Length vs ByteLength:
const arr = new Uint8Array(10);
arr.length; // 10 elements (how many items)
arr.byteLength; // 10 bytes (how much memory)
Views vs Copies:
// View: shares buffer
const view = new Uint8Array(buffer);
// Copy: new independent buffer
const copy = new Uint8Array(existingArray);
What works vs what doesn't:
// ✅ Works: Read, write, iterate, map, filter, slice
arr[0] = 10;
arr.map((x) => x * 2);
arr.slice(1, 4);
// ❌ Doesn't work: Resize operations
arr.push(20); // Error
arr.pop(); // Error
arr.splice(); // Error
Your Accomplishments
You've learned:
✓ Why ArrayBuffer needs Typed Arrays
✓ What "typed" means in Typed Array
✓ How to create Typed Arrays three different ways
✓ How to read and write binary data
✓ How to iterate and transform data
✓ The difference between views and copies
✓ What array operations work and which don't
✓ How to avoid common beginner mistakes
✓ A real-world application: file type detection
You can now:
✓ Create and manipulate binary data in JavaScript
✓ Read file signatures to identify file types
✓ Work with received binary data from files or networks
✓ Choose between views and copies appropriately
✓ Use Typed Arrays efficiently in your applications
✓ Debug common issues with Typed Arrays
What's Next?
You've mastered the fundamentals of Typed Arrays! Now you're ready to expand your knowledge:
Continue the Series
In the next article, you'll learn:
- All 9 Typed Array types in detail (Int8, Uint16, Float32, etc.)
- When to use each type
- Integer types vs floating-point types
- Signed vs unsigned numbers
- Value ranges and wrapping behavior
- Choosing the optimal type for your data
In the final article, you'll master:
- Complete image file parsing (PNG, JPEG)
- Audio file generation (WAV format)
- Canvas pixel manipulation
- Multiple views on the same buffer
- Performance optimization techniques
- Integration with Web APIs
- Production-ready patterns
Related Topics
Expand your binary data knowledge:
- DataView: Flexible Binary Data Access - Learn to handle mixed data types and control byte order (endianness)
- Character Encoding: ASCII, Unicode, UTF-8 - Convert between text and binary data
- Hexadecimal Number System - Read and write binary data in hex format
Deepen your understanding:
- Binary Number System - Master how computers represent all data
- Signed and Unsigned Integers - Understand negative number representation
Practice Exercises
Build these projects to solidify your understanding:
Beginner:
- Byte Inspector - Display any file's first 100 bytes in hex and ASCII
- Color Converter - Convert between RGB arrays and hex color codes
- Simple Checksum - Calculate the sum of all bytes in a file
Intermediate: 4. Magic Number Database - Build a comprehensive file type detector 5. Binary Data Viewer - Create a hex editor interface 6. Data Encoder - Encode text strings into Uint8Array and back
Advanced: 7. Bitmap Image Creator - Generate simple BMP images from scratch 8. Audio Tone Generator - Create multi-tone WAV files 9. Binary Protocol Parser - Implement a custom binary message format
Quick Reference Card
Save this for easy reference:
// CREATE
new Uint8Array(10); // From size
new Uint8Array(buffer); // From buffer (view)
new Uint8Array([1, 2, 3]); // From values (copy)
// ACCESS
arr[0]; // Get element
arr[0] = 255; // Set element
arr.length; // Number of elements
arr.byteLength; // Number of bytes
arr.buffer; // Underlying ArrayBuffer
// ITERATE
for (
let i = 0;
i < arr.length;
i++ // Traditional loop
)
for (const val of arr) // for...of loop
arr.forEach((val, idx) => {}); // forEach method
// TRANSFORM
arr.map((x) => x * 2); // Transform elements
arr.filter((x) => x > 100); // Select elements
arr.reduce((a, b) => a + b, 0); // Aggregate
// COPY
arr.slice(); // Full copy
arr.slice(1, 4); // Partial copy
new Uint8Array(arr); // Copy via constructor
// FILL
arr.fill(0); // Fill all with 0
arr.fill(255, 2, 5); // Fill range with 255
arr.set([10, 20, 30], 0); // Copy array data
// IMPORTANT
// ✓ Fixed size (cannot resize)
// ✓ Values wrap (0-255 for Uint8Array)
// ✓ Views share buffers
// ✓ Copies have independent buffers
Final Thoughts
Congratulations! You've taken your first solid steps into the world of binary data. Typed Arrays might have seemed intimidating at first, but now you understand:
- They're just views into ArrayBuffer—windows that let you see and modify binary data
- The "typed" part ensures you're working with specific number types
- They're array-like enough to feel familiar, but with important differences
- They're essential for working with files, images, audio, networks, and more
Remember the key analogies:
- ArrayBuffer = Storage locker (just the space)
- Typed Array = The door (your way to access it)
- Views = Security cameras (multiple views of the same thing)
- Copies = Photographs (independent snapshots)
With these fundamentals mastered, you're ready to explore the complete family of Typed Arrays and tackle real-world binary data challenges. Keep practicing, experiment with the examples, and don't hesitate to refer back to this guide.
Binary data is no longer a mystery—you've opened the door and stepped inside!