Two-Dimensional Arrays

A plain array is a single row of values — perfect for a list of test scores. But so much of the world isn't a line, it's a grid: a chessboard, a seating plan, a spreadsheet, a Battleships map, the pixels of a photo. All of these have rows and columns, and to store them we need an array that stretches in two directions.

The trick is beautifully simple: a two-dimensional array is just an array whose elements are themselves arrays. The outer array holds the rows; each inner array is one row of the grid. Here is a tiny 3 × 3 noughts-and-crosses board, written as three rows stacked one under another:

const board = [ ["O", " ", "X"], // row 0 [" ", "X", " "], // row 1 ["X", " ", "O"], // row 2 ];

Read it top to bottom and it looks exactly like the game. board is one array with three elements — and each of those elements is itself an array of three symbols. An array of arrays: that's all a 2-D array is.

Two indices: grid[row][col]

A one-dimensional array needs one index to pin down an element. A grid needs two: one to choose the row, then one to choose the column within that row. You write them as two sets of square brackets, row first:

\texttt{grid[row][col]}

Just like ordinary arrays, both indices start at zero. The top-left cell is grid[0][0]; move one column right and it's grid[0][1]; move down one row instead and it's grid[1][0]. The diagram below shows every cell of a 3 × 4 grid labelled with its [row][col] address — trace a few with your finger:

Think of it as a two-step lookup. board[2] hands you back a whole row — the array ["X", " ", "O"]. Then a second index picks one element out of that row: board[2][0] is "X". The first bracket chooses the row; the second reaches into it.

Reading and changing a single cell

Once you know the address, reading a cell is nothing new — and you can change one just by assigning to it. Here we make a board, read a couple of cells, then place a fresh move. Press Run:

const board: string[][] = [ ["O", " ", "X"], [" ", "X", " "], ["X", " ", "O"], ]; console.log("Top-left board[0][0]:", board[0][0]); // "O" console.log("Middle board[1][1]:", board[1][1]); // "X" console.log("Whole middle row, board[1]:", board[1]); board[0][1] = "O"; // play into the empty top-middle cell console.log("After the move, row 0:", board[0]);

Notice the type: string[][] reads as "an array of arrays of strings" — the double [] is the giveaway that you're looking at a grid. A grid of numbers would be number[][].

The .length property still works, and it answers two different questions depending on where you use it. board.length is the number of rows (the size of the outer array); board[0].length is the number of columns (the size of one row). Together they tell you the shape of the whole grid.

const board: string[][] = [ ["O", " ", "X"], [" ", "X", " "], ["X", " ", "O"], ]; console.log("Rows: ", board.length); // 3 console.log("Columns: ", board[0].length); // 3 console.log("So the grid is " + board.length + " by " + board[0].length);

Visiting every cell: nested loops

To visit every element of a one-dimensional array you used one loop. A grid has two dimensions, so you need two loops, one inside the other — a nested loop. The outer loop walks down the rows; for each row, the inner loop walks across the columns. Together they touch every cell exactly once, like reading a book: along each line, then down to the next.

const board: string[][] = [ ["O", " ", "X"], [" ", "X", " "], ["X", " ", "O"], ]; for (let r = 0; r < board.length; r++) { // pick a row for (let c = 0; c < board[r].length; c++) { // then each column in it console.log("board[" + r + "][" + c + "] = '" + board[r][c] + "'"); } }

Read the two conditions carefully. The outer r runs from 0 up to board.length (the row count); the inner c runs from 0 up to board[r].length (that row's column count). Inside, board[r][c] is the current cell. Nine cells, printed in order: row 0 left-to-right, then row 1, then row 2.

Printing a grid the way a human reads it

The loop above printed one cell per line — fine for the computer, ugly for us. Usually you want the inner loop to build up one line of text per row, and only print at the end of each row. That draws the grid as a grid:

const board: string[][] = [ ["O", " ", "X"], [" ", "X", " "], ["X", " ", "O"], ]; for (let r = 0; r < board.length; r++) { let line: string = ""; // start a fresh, empty row for (let c = 0; c < board[r].length; c++) { line = line + board[r][c] + " "; // add each cell to this row's line } console.log(line); // print the finished row }

The shape of the code mirrors the shape of the job: the outer loop is "for each row", and building a fresh line at its top (then printing at its bottom) is what keeps the rows on separate lines. Move that console.log(line) inside the inner loop by mistake and the whole thing collapses.

Working the whole grid: adding up a table

Grids often hold numbers — a table of monthly rainfall, marks in several subjects, sales per shop. To total everything, use the same nested loop but keep a running total (an accumulator), adding each cell as you pass it. Here are three pupils' marks in four subjects:

const marks: number[][] = [ [8, 6, 9, 7], // Amara's four subjects [5, 7, 8, 6], // Ben's [9, 9, 7, 10], // Chloe's ]; let total: number = 0; for (let r = 0; r < marks.length; r++) { for (let c = 0; c < marks[r].length; c++) { total = total + marks[r][c]; // add every cell onto the total } } const cells = marks.length * marks[0].length; console.log("Total of all marks: " + total); console.log("Number of cells: " + cells); console.log("Average mark: " + (total / cells));

You can total just one row (a single pupil) by looping the columns of that row alone, or one column (a single subject across all pupils) by fixing the column and looping the rows. Choosing which index to hold still and which to run is how you slice a grid any way you like:

const marks: number[][] = [ [8, 6, 9, 7], [5, 7, 8, 6], [9, 9, 7, 10], ]; // One row: Chloe's total (row index 2), loop over her columns. let chloe: number = 0; for (let c = 0; c < marks[2].length; c++) chloe = chloe + marks[2][c]; console.log("Chloe's total: " + chloe); // One column: subject 0 across all pupils, loop over the rows, column fixed at 0. let subject0: number = 0; for (let r = 0; r < marks.length; r++) subject0 = subject0 + marks[r][0]; console.log("Subject 0 total: " + subject0);

A digital image is really a 2-D array of pixels. Each cell holds the colour of one tiny dot, addressed by its row and column — image[y][x]. A modest phone photo is a grid roughly 3000 rows by 4000 columns, which is 12 million cells, each storing a colour. When you crop, rotate or brighten a picture, the software is running nested loops over that grid, reading and rewriting cells — exactly the code on this page, just much bigger. Grids of numbers are how computers "see".

Building an empty grid to fill in

Sometimes you don't have the values yet — you want a blank board to fill during a game, or a grid of zeros to count into. You can write out every row by hand for a small grid, or, for a bigger one, build it with loops. This makes a 3 \times 5 grid of zeros:

const rows: number = 3; const cols: number = 5; const grid: number[][] = []; for (let r = 0; r < rows; r++) { const row: number[] = []; // a fresh, empty row for (let c = 0; c < cols; c++) { row.push(0); // fill it with zeros } grid.push(row); // add the finished row to the grid } console.log("Rows:", grid.length, " Columns:", grid[0].length); console.log(grid); grid[1][3] = 7; // set one cell console.log("After grid[1][3] = 7:", grid);

The single biggest 2-D-array mistake is getting the two indices the wrong way round. The order is fixed: row first, then columngrid[row][col]. Swap them and you don't get an error message; you silently read the wrong cell, or fall off the edge of the grid. In a grid with 3 rows and 5 columns, grid[4][1] asks for row 4 — which doesn't exist — while the cell you meant, grid[1][4], was perfectly valid:

const grid = [ /* 3 rows, 5 columns each */ ]; grid[1][4]; // ✓ row 1, column 4 — a real cell grid[4][1]; // ✗ row 4 does not exist — indices swapped!

And remember both indices are zero-based, exactly like a one-dimensional array. A grid of 3 rows has row indices 0, 1, 2 (the last row is grid.length - 1, not grid.length), and a row of 5 columns has column indices 0, 1, 2, 3, 4. Two safe habits: name your loop counters r and c (or row and col) so the order is obvious, and say the address out loud — "row two, column zero" — before you type it.