How to Add Elements to an Array in TypeScript

To add elements to an array in TypeScript, call push() to append to the end, unshift() to add to the beginning, or splice(index, 0, value) to insert at a position in the middle. Those three change the array you already have. If you need a new array instead and want the original left alone, use spread syntax [...cities, "Boston"] or concat(). This guide shows every method with its real output, the type errors TypeScript gives you when the value does not match the array, how to add a whole array to another array, and which method is the right one for each job.

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 followed by node file.js, and the output below is the real output. Reference: Array.prototype.push() on MDN.

Add elements to an array in TypeScript

The three in-place methods differ only in where the new element lands:

const cities: string[] = ["Austin", "Denver", "Seattle"];

cities.push("Boston");             // add to the END
cities.unshift("Phoenix");         // add to the BEGINNING
cities.splice(2, 0, "Chicago");    // INSERT at index 2, remove nothing

console.log(cities);
console.log("length:", cities.length);

Output:

[ 'Phoenix', 'Austin', 'Chicago', 'Denver', 'Seattle', 'Boston' ]
length: 6
Command Prompt showing a TypeScript file compiled with tsc and run with node, adding cities to a string array with push, unshift and splice
push() adds at the end, unshift() at the start, splice() anywhere in between.
What you wantMethodChanges the original?
Add at the endarr.push(value)Yes
Add at the beginningarr.unshift(value)Yes
Insert at index iarr.splice(i, 0, value)Yes
Add one array to anotherarr.push(...other)Yes
Get a new array back[...arr, value] or arr.concat(value)No
Insert at index, new arrayarr.toSpliced(i, 0, value)No

Add an element to the end with push()

push() is the method you will use most. It takes any number of elements, adds them in order, and returns the new length of the array, not the array itself. It works exactly the same on an array of objects, as long as each object matches the declared type:

interface Task {
  id: number;
  title: string;
  done: boolean;
}

const sprint: Task[] = [{ id: 1, title: "Fix checkout bug", done: true }];

const newLength = sprint.push({ id: 2, title: "Update pricing page", done: false });
console.log("push() returned:", newLength);   // the new length, not the array

sprint.push(                                   // several elements in one call
  { id: 3, title: "Write release notes", done: false },
  { id: 4, title: "Deploy to staging", done: false },
);

console.log(sprint.map(t => `#${t.id} ${t.title}`));

Output:

push() returned: 2
[
  '#1 Fix checkout bug',
  '#2 Update pricing page',
  '#3 Write release notes',
  '#4 Deploy to staging'
]

If you came here looking for an append() method: TypeScript does not have one, and it does not need one. Appending means adding at the end, and that is exactly what push() does.

TypeScript checks every element you add

This is the part plain JavaScript tutorials cannot show you. The moment the value does not match the array type, the compiler refuses to build, so a bad record never reaches production:

interface Task {
  id: number;
  title: string;
  done: boolean;
}

const sprint: Task[] = [{ id: 1, title: "Fix checkout bug", done: true }];

sprint.push({ id: 2, title: "Update pricing page" });   // forgot "done"

const prices: number[] = [19.99, 4.5];
prices.push("12");                                       // a string, not a number

tsc output:

push_error.ts(9,13): error TS2345: Argument of type '{ id: number; title: string; }' is not assignable to parameter of type 'Task'.
  Property 'done' is missing in type '{ id: number; title: string; }' but required in type 'Task'.
push_error.ts(12,13): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
Command Prompt showing tsc error TS2345 because a pushed object is missing a required property and a string was pushed into a number array
Two mistakes, caught by tsc before the code ever runs.

That check only exists because the array has a declared element type, which is one good reason for initializing your arrays in TypeScript as const prices: number[] = []. Leave the type off and TypeScript builds it from whatever you put in: const items = [] followed by items.push("a") and items.push(1) compiles without a word and leaves you holding a (string | number)[].

Add an element to the beginning with unshift()

unshift() is the mirror image of push(): same arguments, same return value, but the elements go to the front. When you pass several, they keep the order you wrote them in:

const queue: string[] = ["order-1002", "order-1003"];

queue.unshift("order-1001");                  // one element at the front
console.log(queue);

queue.unshift("order-0999", "order-1000");    // several, in the order you write them
console.log(queue);

console.log("new length:", queue.unshift("order-0998"));

Output:

[ 'order-1001', 'order-1002', 'order-1003' ]
[
  'order-0999',
  'order-1000',
  'order-1001',
  'order-1002',
  'order-1003'
]
new length: 6

Insert an element at a specific index with splice()

splice() takes the start index, how many elements to delete, and then the elements to insert. Pass 0 as the delete count and nothing is removed, so it becomes a pure insert. Its return value is the list of deleted elements, which is why it is empty here:

const steps: string[] = ["Pick items", "Pack box", "Ship"];

steps.splice(2, 0, "Print label");                 // at index 2, delete 0, insert 1
console.log(steps);

steps.splice(1, 0, "Weigh box", "Add invoice");    // insert several
console.log(steps);

console.log("splice() returned:", steps.splice(0, 0));   // the deleted items: none

const plan: string[] = ["Pick items", "Pack box", "Ship"];
const updated = plan.toSpliced(2, 0, "Print label");     // ES2023: a NEW array
console.log(updated);
console.log("original:", plan);

Output:

[ 'Pick items', 'Pack box', 'Print label', 'Ship' ]
[
  'Pick items',
  'Weigh box',
  'Add invoice',
  'Pack box',
  'Print label',
  'Ship'
]
splice() returned: []
[ 'Pick items', 'Pack box', 'Print label', 'Ship' ]
original: [ 'Pick items', 'Pack box', 'Ship' ]
Command Prompt output of TypeScript splice inserting one and then two elements into a string array at a given index, and toSpliced returning a new array
splice() edits the array in place; toSpliced() returns a copy.

toSpliced() is the ES2023 version that leaves the original untouched. It needs "lib": ["es2023", "dom"] (or a newer target) in your tsconfig.json, and Node.js 20 or later at runtime.

Add one array to another array

There are three ways to do this, and they are not the same. concat() and spread syntax build a new array; push() with spread adds the items to the array you already have. Pass the array itself to push() and you add it as a single nested element, which is almost never what you want. If your arrays hold objects and records with the same id should be combined rather than repeated, see merging arrays of objects in TypeScript:

const westCoast: string[] = ["Seattle", "Portland"];
const midwest: string[] = ["Chicago", "Denver"];

console.log(westCoast.concat(midwest));       // new array, both originals untouched
console.log([...westCoast, ...midwest]);      // same result, spread syntax

westCoast.push(...midwest);                   // adds the ITEMS to westCoast
console.log(westCoast);

const regions: (string | string[])[] = ["Seattle"];
regions.push(midwest);                        // adds the ARRAY as one element
console.log(regions);

Output:

[ 'Seattle', 'Portland', 'Chicago', 'Denver' ]
[ 'Seattle', 'Portland', 'Chicago', 'Denver' ]
[ 'Seattle', 'Portland', 'Chicago', 'Denver' ]
[ 'Seattle', [ 'Chicago', 'Denver' ] ]
Command Prompt output showing concat, spread syntax and push with spread adding one TypeScript array to another, and push adding an array as a nested element
The last line is the classic mistake: push(midwest) nests the array.

Why push(…bigArray) can throw a RangeError

Spread turns every element into a separate function argument, and JavaScript engines limit how many arguments a call can take. With a few hundred elements you will never notice; with a few hundred thousand, the call blows the stack:

const big: number[] = new Array(200_000).fill(1);
const target: number[] = [];

try {
  target.push(...big);                   // every element becomes a function argument
} catch (err) {
  console.log((err as Error).name + ":", (err as Error).message);
}

for (const value of big) target.push(value);      // a loop has no argument limit
console.log("loop:", target.length);

const merged: number[] = ([] as number[]).concat(big);
console.log("concat:", merged.length);

Output:

RangeError: Maximum call stack size exceeded
loop: 200000
concat: 200000

The exact cut-off depends on the engine and the stack size, so do not rely on a magic number. For large arrays, use concat() or a plain loop, both of which have no such limit.

Add to an array without changing the original

React state, Redux reducers and anything marked readonly need a new array rather than an edit in place. Spread syntax does that in both directions, and ES2023 adds with() for replacing an element by index:

interface Product {
  sku: string;
  price: number;
}

const cart: readonly Product[] = [{ sku: "A-100", price: 19.99 }];

const withItem: readonly Product[] = [...cart, { sku: "B-200", price: 4.5 }];
console.log(withItem.length, "vs original", cart.length);

const withFirst: Product[] = [{ sku: "C-300", price: 12 }, ...cart];
console.log(withFirst.map(p => p.sku));

const sizes: string[] = ["S", "M", "L"];
console.log(sizes.with(1, "Medium"), sizes);      // ES2023: replace, keep the original

Output:

2 vs original 1
[ 'C-300', 'A-100' ]
[ 'S', 'Medium', 'L' ] [ 'S', 'M', 'L' ]

A readonly array in TypeScript does not even have the mutating methods, so the mistake is caught at compile time:

interface Product {
  sku: string;
  price: number;
}

const cart: readonly Product[] = [{ sku: "A-100", price: 19.99 }];
cart.push({ sku: "B-200", price: 4.5 });

tsc output:

readonly_push.ts(7,6): error TS2339: Property 'push' does not exist on type 'readonly Product[]'.

Tuples let push() through, so the length is not protected

A tuple type fixes the length when you create the array, but push() still compiles, because at runtime a tuple is an ordinary array. Mark it readonly if the length matters:

const coordinates: [number, number] = [39.74, -104.99];   // Denver

coordinates.push(1609);          // TypeScript allows this: a tuple is an array at runtime
console.log(coordinates, "length:", coordinates.length);

const fixed: readonly [number, number] = [47.61, -122.33];
// fixed.push(0);                // readonly tuple: push does not exist, so this is blocked
console.log(fixed);

Output:

[ 39.74, -104.99, 1609 ] length: 3
[ 47.61, -122.33 ]

Add a value only if it is not already there

Arrays allow duplicates, so adding unique values takes one extra check. For a handful of items, checking whether the array contains the value with includes() is clearest; for a long list, a Set is faster and also handy for getting unique values out of an array:

const tags: string[] = ["python", "typescript"];

function addUnique<T>(list: T[], value: T): T[] {
  if (!list.includes(value)) list.push(value);
  return list;
}

console.log(addUnique(tags, "react"));
console.log(addUnique(tags, "python"));        // already there, nothing added

const combined = [...new Set([...tags, "django", "react", "pandas"])];
console.log(combined);

Output:

[ 'python', 'typescript', 'react' ]
[ 'python', 'typescript', 'react' ]
[ 'python', 'typescript', 'react', 'django', 'pandas' ]

includes() compares objects by reference, so two objects with identical properties both get added. Compare a property, such as list.some(p => p.sku === item.sku), when the array holds objects.

Add by index, and the empty slots it can leave

arr[arr.length] = value is the same thing as push(). Writing past the end is not: it stretches the array and leaves real gaps that survive map(), which is a common source of unexpected empty or null values in an array:

const scores: number[] = [10, 20];

scores[scores.length] = 30;          // the same as push()
console.log(scores);

scores[5] = 99;                       // index 3 and 4 were never set
console.log(scores, "length:", scores.length, "| scores[3]:", scores[3]);
console.log(scores.map(n => n * 2));  // the gaps stay empty

Output:

[ 10, 20, 30 ]
[ 10, 20, 30, <2 empty items>, 99 ] length: 6 | scores[3]: undefined
[ 20, 40, 60, <2 empty items>, 198 ]

Append in a loop, or with one reusable helper

Real data rarely arrives clean. When rows have to be validated, transformed or filtered before they go in, a loop with push() is the clearest way to collect the good ones. And if you add items in many places, a small generic helper gives you one immutable append for every element type at once:

interface SalesRecord {
  orderId: string;
  total: number;
}

// one helper for any element type, and it never touches the array you pass in
function appendItem<T>(items: readonly T[], item: T): T[] {
  return [...items, item];
}

const cities: string[] = ["Seattle", "Denver"];
console.log(appendItem(cities, "Austin"), cities);

const imported: SalesRecord[] = [
  { orderId: "ORD-1001", total: 125.5 },
  { orderId: "ORD-1002", total: -20 },
  { orderId: "ORD-1003", total: 340 },
];

const valid: SalesRecord[] = [];
for (const sale of imported) {
  if (sale.total > 0) valid.push(sale);      // append only the rows that pass
}

console.log(valid);
console.log("skipped:", imported.length - valid.length);

Output:

[ 'Seattle', 'Denver', 'Austin' ] [ 'Seattle', 'Denver' ]
[
  { orderId: 'ORD-1001', total: 125.5 },
  { orderId: 'ORD-1003', total: 340 }
]
skipped: 1

appendItem takes a readonly T[], which is a promise in the type system that it will not modify what you hand it, and returns a fresh T[]. Pass a string[] and T becomes string; pass an array of records and T becomes that record type, so a wrong value is still a compile error.

push() vs unshift() vs spread: which is faster?

The difference is not academic once an array gets long. push() writes at the end, while unshift() has to shift every existing element one place to the right, and rebuilding the array with spread inside a TypeScript for loop copies the whole array on every pass:

function time(label: string, fn: () => void): void {
  const start = performance.now();
  fn();
  console.log(label.padEnd(22), (performance.now() - start).toFixed(1).padStart(8), "ms");
}

const N = 100_000;

time("push()", () => { const a: number[] = []; for (let i = 0; i < N; i++) a.push(i); });
time("a[a.length] = i", () => { const a: number[] = []; for (let i = 0; i < N; i++) a[a.length] = i; });
time("unshift()", () => { const a: number[] = []; for (let i = 0; i < N; i++) a.unshift(i); });
time("a = [...a, i] (20k)", () => { let a: number[] = []; for (let i = 0; i < 20_000; i++) a = [...a, i]; });
Command Prompt output timing push, index assignment, unshift and spread inside a loop adding 100,000 numbers to a TypeScript array
One run on Node.js 22: adding 100,000 numbers each way.

Adding 100,000 numbers with push() or by index takes single-digit milliseconds. The same 100,000 through unshift() takes several hundred, and rebuilding the array with [...a, i] needs more than a second for only a fifth as many elements. The exact figures move from run to run, but the ranking never does: add at the end when you can, and when you really do need items at the front, collect them and call unshift(...batch) once instead of one at a time.

Here are more TypeScript array tutorials worth a look:

Frequently asked questions

How do I add an element to an array in TypeScript?

Call arr.push(value) to add it to the end. The value must match the array type, so const names: string[] accepts only strings.

How do I append to an array in TypeScript?

There is no append() method. Appending means adding at the end, so use arr.push(value), or [...arr, value] when the original must stay as it is.

How do I add an element to the beginning of an array?

Use arr.unshift(value), or build a new array with [value, ...arr] if you need to keep the original.

How do I insert into an array at a specific index in TypeScript?

Use arr.splice(index, 0, value). The 0 means delete nothing, so the value is simply inserted at that index.

How do I push an array into another array?

a.push(...b) adds the items of b to a. a.push(b) adds b as one nested element instead.

What is the difference between push() and concat()?

push() changes the array and returns its new length. concat() leaves both arrays alone and returns a new one.

Why does TypeScript say push does not exist on a readonly array?

A readonly array has no mutating methods. Create a new array with [...arr, value], or drop the readonly modifier.