To initialize an array in TypeScript, declare the element type and give it a value: const cities: string[] = ["Austin", "Denver"], or const ids: string[] = [] to start empty. Array<string> means the same as string[]. For a fixed number of slots use Array.from({ length: n }, ...) rather than new Array(n), which leaves holes. This guide covers every way to create one, including arrays of objects with defaults, two-dimensional arrays, tuples and readonly arrays, and the shared-reference trap that catches everyone the first time.
Every example was compiled and run with TypeScript 5.2.2 and Node.js v22.22.2 in the Windows Command Prompt, with tsc --target es2022 --lib es2023,dom --strict file.ts and then node file.js. The output is the real output. Reference: Arrays in the TypeScript Handbook.
Declare and initialize an array
The annotation goes on the variable, the values go on the right. TypeScript can infer the type from the values, but writing it means an empty array still knows what it will hold:
// with values: the type is inferred, but writing it is clearer
const cities: string[] = ["Austin", "Denver", "Seattle"];
const totals: number[] = [249.99, 89.5];
// empty, ready to fill later
const ids: string[] = [];
ids.push("ORD-1001");
// the generic spelling, identical in meaning
const flags: Array<boolean> = [true, false];
console.log(cities.length, totals[0], ids, flags);
// inferred without an annotation
const mixed = ["Austin", 42, true]; // (string | number | boolean)[]
console.log(mixed);
Output:
3 249.99 [ 'ORD-1001' ] [ true, false ]
[ 'Austin', 42, true ]
| What you want | Write |
|---|---|
| Values you already have | const a: string[] = ["x", "y"] |
| An empty array to fill later | const a: string[] = [] |
| The generic spelling | const a: Array<string> = [] |
| n copies of a value | new Array(n).fill(0) |
| n generated values | Array.from({ length: n }, (_, i) => i) |
| n objects, each its own | Array.from({ length: n }, () => ({ ... })) |
| A grid | Array.from({ length: rows }, () => new Array(cols).fill(0)) |
| Fixed length and types | const p: [number, number] = [0, 0] |
new Array() and the holes it leaves
new Array(3) does not create three undefined values: it creates an array of length 3 with nothing in it. Those empty slots are skipped by map(), forEach() and friends, which is why the third line below returns holes rather than indexes. One number is a length, two or more are values:
// one number means LENGTH, not a value
const three = new Array(3);
console.log(three, three.length);
// two or more arguments are the values
const values = new Array(3, 4);
console.log(values);
// the holes are not undefined values: map skips them
console.log(new Array(3).map((_, i) => i));
// fill() first, and then every slot is real
console.log(new Array(3).fill(0));
console.log(Array(3).fill("pending"));
// or build the values as you go
console.log(Array.from({ length: 5 }, (_, i) => i * 2));
Output:
[ <3 empty items> ] 3
[ 3, 4 ]
[ <3 empty items> ]
[ 0, 0, 0 ]
[ 'pending', 'pending', 'pending' ]
[ 0, 2, 4, 6, 8 ]
<3 empty items> is not the same as three zeros.fill() or Array.from() both fix it. Array.from() is the one to reach for when each slot should be different, because it calls your function once per index, as any loop over the array would.
Arrays of objects with default values
Here is the trap. fill() puts the same object in every slot, so changing one changes them all. Array.from() runs the function per slot and gives each one its own object. Primitives are unaffected, because they are copied by value. There is more on typing these in arrays of objects in TypeScript:
interface Slot {
id: number;
taken: boolean;
}
// fill() with an OBJECT puts the same object in every slot
const shared: Slot[] = new Array(3).fill({ id: 0, taken: false });
shared[0].taken = true;
console.log(shared);
// Array.from runs the function once per slot, so each object is its own
const separate: Slot[] = Array.from({ length: 3 }, (_, i) => ({ id: i, taken: false }));
separate[0].taken = true;
console.log(separate);
// numbers and strings are values, so fill() is fine for them
console.log(new Array(4).fill(0), new Array(2).fill("n/a"));
Output:
[
{ id: 0, taken: true },
{ id: 0, taken: true },
{ id: 0, taken: true }
]
[
{ id: 0, taken: true },
{ id: 1, taken: false },
{ id: 2, taken: false }
]
[ 0, 0, 0, 0 ] [ 'n/a', 'n/a' ]
taken: true change affected all three rows. Bottom: only the first.Two-dimensional arrays
A 2D array is typed number[][], read as “an array of arrays of numbers”. The same shared-reference problem appears here, one level up: filling with an inner array puts one row in every position, so writing to grid[0][0] writes to all of them:
// a 2D array is an array of arrays
const grid: number[][] = [
[1, 2, 3],
[4, 5, 6],
];
console.log(grid[1][2], grid.length, grid[0].length);
// the classic trap: every row is the SAME array
const shared: number[][] = new Array(3).fill(new Array(3).fill(0));
shared[0][0] = 9;
console.log(shared);
// build each row separately
const rows: number[][] = Array.from({ length: 3 }, () => new Array(3).fill(0));
rows[0][0] = 9;
console.log(rows);
// typed with an interface, which is what a grid of cells usually needs
interface Cell {
value: number;
locked: boolean;
}
const board: Cell[][] = Array.from({ length: 2 }, (_, row) =>
Array.from({ length: 2 }, (_, col) => ({ value: row * 2 + col, locked: false })),
);
console.log(JSON.stringify(board));
Output:
6 2 3
[ [ 9, 0, 0 ], [ 9, 0, 0 ], [ 9, 0, 0 ] ]
[ [ 9, 0, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] ]
[[{"value":0,"locked":false},{"value":1,"locked":false}],[{"value":2,"locked":false},{"value":3,"locked":false}]]
Building an array from something else
Array.from() turns anything iterable or array-like into a real array, and takes an optional mapping function. This is how strings become character arrays, how a Set becomes a deduplicated array, and how a Map becomes entries:
// from a string
console.log(Array.from("Austin"));
// from a Set, which is how you deduplicate
console.log(Array.from(new Set(["a", "b", "a"])));
// from a Map, and from its keys or values
const prices = new Map<string, number>([["KB-1", 49.99], ["MN-9", 229]]);
console.log(Array.from(prices));
console.log([...prices.keys()], [...prices.values()]);
// from anything array-like or iterable, with a mapping step
console.log(Array.from({ length: 3 }, (_, i) => `row-${i + 1}`));
// spreading does the same for iterables
console.log([..."TX", ...new Set([1, 1, 2])]);
Output:
[ 'A', 'u', 's', 't', 'i', 'n' ]
[ 'a', 'b' ]
[ [ 'KB-1', 49.99 ], [ 'MN-9', 229 ] ]
[ 'KB-1', 'MN-9' ] [ 49.99, 229 ]
[ 'row-1', 'row-2', 'row-3' ]
[ 'T', 'X', 1, 2 ]
Tuples and readonly arrays
A tuple fixes the length and the type of each position, which is what you want for coordinates, key-value pairs and anything returned as a pair. readonly stops the array being changed at all, and as const also locks the values into literal types, which is the basis of checking a value against a list of allowed options:
// a tuple: fixed length, and a type per position
const point: [number, number] = [39.74, -104.99];
const entry: [string, number] = ["KB-1", 49.99];
// named positions read better in editors and error messages
const range: [start: number, end: number] = [0, 10];
console.log(point[0], entry[1], range[1] - range[0]);
// readonly: no push, no reassigning positions
const regions: readonly string[] = ["TX", "CO"];
const fixed: readonly [number, number] = [0, 0];
console.log(regions.join(", "), fixed.length);
// as const makes the values literal types too
const statuses = ["pending", "shipped"] as const;
type Status = (typeof statuses)[number];
const current: Status = "shipped";
console.log(current, statuses.length);
Output:
39.74 49.99 10
TX, CO 2
shipped 2
Why the annotation matters on an empty array
An annotated empty array still rejects the wrong type the moment you push into it, which is the whole reason for writing it:
const cities: string[] = [];
cities.push("Austin");
cities.push(42); // the array was declared as string[]
tsc output:
empty_array_type_error.ts(4,13): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
Without the annotation, TypeScript infers the type from what you add, so mistakes go unnoticed until something downstream breaks. In React the same rule applies to state: useState<Order[]>([]), not useState([]). Once the array exists, adding elements and removing them both keep the type.
More TypeScript array guides worth reading:
- Arrays of objects in TypeScript
- Add or append elements to an array
- Loop through an array
- Check if an array is null or empty
- Readonly arrays in TypeScript
Frequently asked questions
How do I initialize an array in TypeScript?
Declare the element type and assign a value: const cities: string[] = ["Austin"], or = [] for an empty one.
What is the difference between string[] and Array<string>?
Nothing. They are two spellings of the same type, and both are checked identically.
How do I create an array of a fixed size?
new Array(5).fill(0) for repeated values, or Array.from({ length: 5 }, (_, i) => i) when each value differs.
Why is new Array(3) empty rather than three undefined values?
It sets the length without creating elements. Those holes are skipped by map() and forEach(), so call fill() first.
Why does changing one object change all of them?
fill() stores the same object reference in every slot. Use Array.from({ length: n }, () => ({ ... })) to build one per slot.
How do I create a 2D array in TypeScript?
Type it number[][] and build the rows separately: Array.from({ length: rows }, () => new Array(cols).fill(0)).
How do I declare an array with fixed positions and types?
Use a tuple: const point: [number, number] = [0, 0]. Add readonly to stop it being changed.
Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn