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
push() adds at the end, unshift() at the start, splice() anywhere in between.| What you want | Method | Changes the original? |
|---|---|---|
| Add at the end | arr.push(value) | Yes |
| Add at the beginning | arr.unshift(value) | Yes |
Insert at index i | arr.splice(i, 0, value) | Yes |
| Add one array to another | arr.push(...other) | Yes |
| Get a new array back | [...arr, value] or arr.concat(value) | No |
| Insert at index, new array | arr.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'.
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' ]
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' ] ]
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]; });
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:
- Filter arrays in TypeScript
- Sort an array of objects by property
- Remove duplicates from an array
- Find an object in a TypeScript array
- Check if an array is null or empty
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.
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