TypeScript 2D Arrays: Create, Loop and Type Them with Interfaces

A 2D array in TypeScript is an array of arrays, typed number[][] or Array<Array<number>>. Each inner array is a row, and you read a cell with two indexes, row first: sales[1][2].

const sales: number[][] = [
  [1240.5, 980, 1105.25],   // Austin
  [860, 915.4, 1020],       // Dallas
];

sales[1][2];   // 1020: Dallas, third day

The outputs below came straight out of the Windows Command Prompt, compiled with tsc --strict on TypeScript 5.2.2 and Node.js v22.22.2.

You’ll build empty grids without the shared-row bug, loop with indexes, type a grid with an interface, handle jagged and 3D arrays, and fix the error 2D arrays are known for.

Declare a 2D array in TypeScript

There are two spellings of the same type. number[][] is the short one you’ll see most, and Array<Array<number>> says exactly the same thing.

// weekly sales in USD: one row per store, one column per weekday
const sales: number[][] = [
  [1240.5, 980, 1105.25, 1410, 1890.75],   // Austin
  [860, 915.4, 1020, 990, 1350.2],         // Dallas
  [1530, 1475.6, 1610, 1702.35, 2210],     // Houston
];

// row first, then column
console.log("Houston, Friday:", sales[2][4]);
console.log("stores:", sales.length, "| days:", sales[0].length);

// the generic spelling is exactly the same type
const sameSales: Array<Array<number>> = sales;
console.log("same array:", sameSales === sales);

// without an annotation, TypeScript infers string[][]
const board = [["X", "O"], ["O", "X"]];
console.log("board[1][0]:", board[1][0]);

Output:

Houston, Friday: 2210
stores: 3 | days: 5
same array: true
board[1][0]: O
Command Prompt output of a TypeScript 2D array of weekly store sales, showing one cell, the row and column counts and the inferred string[][] type
Row 2, column 4 is Houston on Friday. Both spellings point at the same array.

If you leave the annotation off, TypeScript infers it from the values, which is why board came out as string[][]. I still write it on anything that starts empty, because an empty [] can’t tell TypeScript what it’ll hold.

Create an empty 2D array of a fixed size

This is where most 2D array bugs come from. The obvious one-liner, new Array(3).fill(new Array(3).fill("-")), builds one row and puts that same row in all three slots.

// a 3 x 3 tic-tac-toe board, the wrong way
const shared: string[][] = new Array(3).fill(new Array(3).fill("-"));
shared[0][0] = "X";
console.log("with fill():");
shared.forEach(row => console.log(" ", row.join(" ")));
console.log("  same row object?", shared[0] === shared[1]);

// the right way: Array.from builds a new row for every index
const board: string[][] = Array.from({ length: 3 }, () => new Array(3).fill("-"));
board[0][0] = "X";
console.log("with Array.from():");
board.forEach(row => console.log(" ", row.join(" ")));
console.log("  same row object?", board[0] === board[1]);

// any size, with a value worked out per cell
const timesTable: number[][] = Array.from({ length: 2 }, (_, r) =>
  Array.from({ length: 4 }, (_, c) => (r + 1) * (c + 1)),
);
console.log(timesTable);

Output:

with fill():
  X - -
  X - -
  X - -
  same row object? true
with Array.from():
  X - -
  - - -
  - - -
  same row object? false
[ [ 1, 2, 3, 4 ], [ 2, 4, 6, 8 ] ]
Command Prompt output showing a TypeScript 2D array built with fill() where one move marks every row, next to an Array.from grid where only one cell changes
One move on the fill() board marked the first cell of every row.

fill() evaluates its argument once and copies the reference, so every row points at the same array. Array.from({ length: n }, () => ...) calls your function per index, so each row is a fresh array.

It’s the 2D array bug I see most in code reviews. Primitives are safe to fill(), which is why the inner new Array(3).fill("-") is fine; only the row array must not be shared.

The same trap catches arrays of objects, and initializing an array in TypeScript walks through it for flat arrays too.

Read and update cells in a 2D array

Two indexes, always row then column. A single index gives you a whole row, which you can loop over, destructure or pass around like any other array.

const sales: number[][] = [
  [1240.5, 980, 1105.25],
  [860, 915.4, 1020],
];

// read one cell: row, then column
console.log("row 0, column 2:", sales[0][2]);

// update one cell
sales[1][0] = 905;
console.log("row 1 after update:", sales[1]);

// the last row, and the last cell of a row
console.log("last row:", sales[sales.length - 1]);
console.log("last cell of row 0:", sales[0].at(-1));

// a row is an ordinary array, so you can destructure it
const [mon, tue, wed] = sales[0];
console.log(mon, tue, wed);

Output:

row 0, column 2: 1105.25
row 1 after update: [ 905, 915.4, 1020 ]
last row: [ 905, 915.4, 1020 ]
last cell of row 0: 1105.25
1240.5 980 1105.25

at(-1) is the tidy way to reach the last element. It works on the outer array too, so sales.at(-1) is the last row.

Loop through a TypeScript 2D array with row and column indexes

There are three good options, and the choice comes down to whether you need the indexes.

const stores = ["Austin", "Dallas", "Houston"];
const days = ["Mon", "Tue", "Wed", "Thu", "Fri"];
const sales: number[][] = [
  [1240.5, 980, 1105.25, 1410, 1890.75],
  [860, 915.4, 1020, 990, 1350.2],
  [1530, 1475.6, 1610, 1702.35, 2210],
];

// 1. classic for loops, when you need both indexes
for (let r = 0; r < sales.length; r++) {
  for (let c = 0; c < sales[r].length; c++) {
    if (sales[r][c] >= 1700) console.log(`${stores[r]} ${days[c]}: $${sales[r][c]}`);
  }
}

// 2. for...of, when you only need the values
let weekTotal = 0;
for (const row of sales) {
  for (const amount of row) weekTotal += amount;
}
console.log("week total:", weekTotal.toLocaleString("en-US", { style: "currency", currency: "USD" }));

// 3. forEach, which passes the row index in for you
sales.forEach((row, r) => console.log(stores[r].padEnd(8), row.join(" | ")));

Output:

Austin Fri: $1890.75
Houston Thu: $1702.35
Houston Fri: $2210
week total: $20,290.05
Austin   1240.5 | 980 | 1105.25 | 1410 | 1890.75
Dallas   860 | 915.4 | 1020 | 990 | 1350.2
Houston  1530 | 1475.6 | 1610 | 1702.35 | 2210
Command Prompt output of nested for loops, for...of and forEach looping through a TypeScript 2D array of store sales
Days over $1,700, the week’s total in US dollars, then every row printed with its store name.

Use the classic for loop when you need r and c, as the first block does. for...of reads best when you only want the values, and forEach hands you the index without the counter.

One habit worth keeping: test c < sales[r].length, not c < sales[0].length. Rows aren’t guaranteed to be the same length. There’s more on each loop style in looping through arrays in TypeScript.

How do you type a 2D array with an interface?

That’s what most people searching for a TypeScript 2D array interface really want to know. An interface can’t usefully be the grid, but it’s the right tool for the cells and for the object that holds the grid.

So you write the cell type as an interface and use it as Seat[][]. Here’s a seat map for a show, with prices in US dollars:

// each cell is an object, so describe it with an interface
interface Seat {
  label: string;
  price: number;    // USD
  booked: boolean;
}

// and describe the object that holds the grid
interface SeatMap {
  venue: string;
  showDate: string;
  seats: Seat[][];  // rows of seats
}

const rowNames = ["A", "B", "C"];

const show: SeatMap = {
  venue: "Main Street Playhouse",
  showDate: "2026-10-16",
  seats: rowNames.map((row, r) =>
    Array.from({ length: 4 }, (_, c) => ({
      label: `${row}${c + 1}`,
      price: r === 0 ? 89 : 59,
      booked: false,
    })),
  ),
};

show.seats[1][2].booked = true;   // B3 sells
show.seats[0][0].booked = true;   // A1 sells

console.log(`${show.venue}, ${show.showDate}`);
show.seats.forEach(row => console.log(" ", row.map(s => (s.booked ? "--" : s.label)).join(" ")));

const available = show.seats.flat().filter(s => !s.booked);
console.log(`${available.length} seats left, worth $${available.reduce((sum, s) => sum + s.price, 0)}`);

// a type alias names the grid itself
type Matrix = number[][];
const identity: Matrix = [[1, 0], [0, 1]];
console.log(identity);

Output:

Main Street Playhouse, 2026-10-16
  -- A2 A3 A4
  B1 B2 -- B4
  C1 C2 C3 C4
10 seats left, worth $680
[ [ 1, 0 ], [ 0, 1 ] ]
Command Prompt output of a TypeScript 2D array typed with an interface, printing a theater seat map with booked seats and the value of seats left
Seat[][] gives every cell a checked shape. Booked seats print as --.

For the grid itself, a type alias reads best: type Matrix = number[][]. An interface that extends Array<number[]> does compile, but I’ve never seen it make code clearer than the alias.

If each cell holds arrays of its own, interfaces with nested arrays of objects covers that shape, and arrays of objects in TypeScript covers the one-row version.

Add and remove rows and columns in a 2D array

Rows are easy because they’re the outer array: push() adds one and splice() removes one. Columns live inside every row, so you change each row in a loop.

const stores = ["Austin", "Dallas"];
const days = ["Mon", "Tue", "Wed"];
const sales: number[][] = [
  [1240.5, 980, 1105.25],
  [860, 915.4, 1020],
];

// add a row: a new store
stores.push("Houston");
sales.push([1530, 1475.6, 1610]);

// add a column: a new day on every row
days.push("Thu");
const thursday = [1410, 990, 1702.35];
sales.forEach((row, r) => row.push(thursday[r]));

console.log(days.join(", "));
sales.forEach((row, r) => console.log(stores[r].padEnd(8), row));

// remove a row (Dallas) and a column (Mon)
sales.splice(1, 1);
stores.splice(1, 1);
sales.forEach(row => row.splice(0, 1));
days.splice(0, 1);

console.log("after removing Dallas and Monday:");
console.log(days.join(", "));
sales.forEach((row, r) => console.log(stores[r].padEnd(8), row));

Output:

Mon, Tue, Wed, Thu
Austin   [ 1240.5, 980, 1105.25, 1410 ]
Dallas   [ 860, 915.4, 1020, 990 ]
Houston  [ 1530, 1475.6, 1610, 1702.35 ]
after removing Dallas and Monday:
Tue, Wed, Thu
Austin   [ 980, 1105.25, 1410 ]
Houston  [ 1475.6, 1610, 1702.35 ]

Keep any label arrays in step, as stores and days are here. Forgetting to remove a store’s name along with its row is how reports end up off by one.

Both moves are ordinary array methods underneath, so adding elements and removing them apply row by row.

Sum, transpose and flatten a 2D array

Most real work on a grid comes down to totals in on