TypeScript Array of Objects: Declare, Find, Sort and Group

Declare a typed array of objects with an interface or type alias, then add, find, filter, sort, group and update its objects without surprises.

A TypeScript array of objects is an array whose elements are all objects of the same shape, for example a list of people where each person has a name and an age. We describe the shape once with an interface or a type alias and declare the array as Person[]. From then on, the compiler checks every object we add to the array and every property we read from it, so a typo such as p.nmae or a missing age becomes a compile error instead of a runtime bug.

This tutorial covers how to declare the type of an array of objects in TypeScript, how to add, remove and update objects, how to find, filter and sort them by a property, and how to group them. It also explains the details that cause bugs with object arrays: index access that returns undefined, includes() comparing references, and copies that share the same objects.

All code was compiled with TypeScript 7.0.2 (strict mode, ES2024 library) and run on Node.js 22.

Most work on an array of objects uses a handful of array methods together with a callback that reads one property. A reader who needs one operation can copy it from this block.

interface Person {
  name: string;
  age: number;
}

const people: Person[] = [
  { name: "Lokesh", age: 37 },
  { name: "Raj", age: 35 },
];

// 1. Add an object
people.push({ name: "John", age: 40 });                 // length = 3

// 2. Find one object by a property
const raj = people.find((p) => p.name === "Raj");       // raj = { name: "Raj", age: 35 }

// 3. Filter objects by a property
const over36 = people.filter((p) => p.age > 36);        // over36 = [Lokesh, John]

// 4. Sort by a property into a new array
const byAge = people.toSorted((a, b) => a.age - b.age); // byAge = [Raj, Lokesh, John]

// 5. Get one property from every object
const names = people.map((p) => p.name);                // names = ["Lokesh", "Raj", "John"]

// 6. Update an object found by a property
const i = people.findIndex((p) => p.name === "Raj");    // i = 1
people[i].age = 36;                                     // Raj is now 36

// 7. Remove an object by a property
const others = people.filter((p) => p.name !== "John"); // others = [Lokesh, Raj]

// 8. Loop over the objects
for (const { name, age } of people) {
  console.log(name, age);                               // Lokesh 37, Raj 36, John 40
}

In the comments, a list such as [Lokesh, John] is short for the matching Person objects.

1. Declaring the Type of an Array of Objects

TypeScript has two equivalent ways to write an array type:

  • Person[]
  • Array<Person>

Both produce the same type, and most code bases pick Person[] because it is shorter. The element type itself can come from an interface, a type alias, or an inline object type.

// 1. Interface and T[] syntax
interface Person {
  name: string;
  age: number;
}
const people: Person[] = [{ name: "Lokesh", age: 37 }];

// 2. Generic Array<T> syntax, same type
const people2: Array<Person> = [{ name: "Raj", age: 35 }];

// 3. Type alias with an optional property
type Fruit = { name: string; count: number; color?: string };
const fruits: Fruit[] = [
  { name: "apple", count: 5, color: "red" },
  { name: "banana", count: 3 },
];

// 4. Inline object type
const points: { x: number; y: number }[] = [{ x: 1, y: 2 }];

// 5. Inferred type: { name: string; age: number }[]
const team = [
  { name: "John", age: 40 },
  { name: "Raj", age: 35 },
];

We use a named interface or type alias when the same shape appears in more than one place, such as a function parameter and a return type. An inline type fits a one-off array. The question mark in color? makes the property optional, so the banana object without a color is still valid.

Inference (option 5) works for a filled array. For an empty array, we always write the type, as in const people: Person[] = []. Without it, TypeScript has to guess the element type from later code, and the guess is often wider than we want.

The annotation is what makes the compiler check each object. Below, one object misses a required property and another has a property the interface does not declare.

const people: Person[] = [
  { name: "Lokesh", age: 37 },
  { name: "Raj" },
  { name: "John", age: 40, city: "Delhi" },
];
src/err.ts(3,3): error TS2741: Property 'age' is missing in type '{ name: string; }' but required in type 'Person'.
src/err.ts(4,28): error TS2353: Object literal may only specify known properties, and 'city' does not exist in type 'Person'.

Sometimes the array comes first and there is no named type yet. The indexed access type (typeof team)[number] reads the element type out of an existing array: typeof team is the array type, and [number] picks the type of any element.

type Member = (typeof team)[number];                    // { name: string; age: number }
const member: Member = { name: "Lokesh", age: 37 };

2. Adding, Removing and Changing Objects

The methods that add and remove elements work the same for objects as for numbers or strings. Each of them changes the original array. The articles on adding items and removing items cover them in more detail; here we focus on what changes when the elements are objects.

const people: Person[] = [
  { name: "Lokesh", age: 37 },
  { name: "Raj", age: 35 },
  { name: "John", age: 40 },
];

// 1. Add at the end, at the start, at an index
people.push({ name: "Amit", age: 30 });                 // [Lokesh, Raj, John, Amit]
people.unshift({ name: "Neha", age: 28 });              // [Neha, Lokesh, Raj, John, Amit]
people.splice(2, 0, { name: "Ravi", age: 33 });         // [Neha, Lokesh, Ravi, Raj, John, Amit]

// 2. Remove from the end, from the start, at an index
const last = people.pop();                              // last = Amit
const first = people.shift();                           // first = Neha
const removed = people.splice(1, 1);                    // removed = [Ravi]

// 3. Change a property of the object at an index
people[0].age = 38;                                     // Lokesh is now 38

// 4. Find the index by a property, then replace the object
const i = people.findIndex((p) => p.name === "John");   // i = 2
if (i !== -1) {
  people[i] = { ...people[i], age: 41 };                // John is now 41
}

The type of pop() and shift() is Person | undefined, because the array can be empty. In splice(2, 0, item), the first argument is the position, the second is how many elements to delete, and the rest are the elements to insert. The findIndex() method returns -1 when nothing matches, and writing to people[-1] would not update anyone, so we check the index first.

Option 4 writes a new object into the slot instead of changing the old one. The spread syntax …people[i] copies all fields, and age: 41 overrides one of them. Both styles work; the difference matters when other code holds a reference to the old object, which section 6 explains.

2.1. Index Access Can Return undefined

Reading an index that does not exist returns undefined at runtime. In the default strict settings, however, the type of people[5] is still Person, so the compiler does not warn us, and reading people[5].name throws a TypeError.

const p = people[5];                                    // type Person, value undefined
const q = people.at(5);                                 // type Person | undefined
const lastPerson = people.at(-1);                       // lastPerson = John

The at() method returns Person | undefined, so the compiler forces a check before use, and a negative index counts from the end. To get the same check on every bracket access, we can turn on the noUncheckedIndexedAccess compiler option. With it, people[5].name fails to compile with “error TS18048: ‘p’ is possibly ‘undefined’.” The option is not included in strict. It also adds undefined to reads inside loops where the index is always valid, so some existing code needs extra checks after we turn it on.

3. Finding and Filtering Objects by a Property

Searching an array of objects means passing a callback that tests one or more properties. The method decides how many results we get: find() returns the first match, findLast() (ES2023) returns the last match, and filter() returns all matches in a new array. The find() and filter() articles cover each method on its own.

// people = [Lokesh 37, Raj 35, John 40]

// 1. First and last match
const first = people.find((p) => p.age > 36);           // first = Lokesh
const last = people.findLast((p) => p.age > 36);        // last = John
const none = people.find((p) => p.age > 50);            // none = undefined

// 2. All matches
const over36 = people.filter((p) => p.age > 36);        // over36 = [Lokesh, John]

// 3. Yes/no checks
const hasRaj = people.some((p) => p.name === "Raj");    // hasRaj = true
const allAdults = people.every((p) => p.age >= 18);     // allAdults = true

// 4. includes() compares references, not fields
const copy = { name: "Raj", age: 35 };
const found = people.includes(copy);                    // found = false
const same = people.includes(people[1]);                // same = true

The return type of find() is Person | undefined, so we handle the no-match case with optional chaining (first?.name) or an if check. The filter() method never returns undefined; with no match it returns an empty array.

The includes() and indexOf() methods compare elements with the strict equality rule. For objects, that means the same reference in memory, not the same field values. An object literal with identical field values is still a separate object, so found is false. To check whether an equal object exists, we use some() with a test on the fields that identify it, such as the name.

4. Sorting by a Property

Sorting an array of objects always needs a compare function, because JavaScript does not know which property to sort by. The function receives two objects a and b and returns a negative number when a comes first, a positive number when b comes first, and 0 when the order does not matter.

The ES2023 method toSorted() returns a new sorted array. The older sort() method sorts the original array in place and returns it.

// people = [Lokesh 37, Raj 35, John 40]

// 1. New sorted arrays (ES2023); people is unchanged
const byAge = people.toSorted((a, b) => a.age - b.age);                 // [Raj, Lokesh, John]
const oldestFirst = people.toSorted((a, b) => b.age - a.age);           // [John, Lokesh, Raj]
const byName = people.toSorted((a, b) => a.name.localeCompare(b.name)); // [John, Lokesh, Raj]

// 2. sort() changes the original array
people.sort((a, b) => a.age - b.age);                   // people = [Raj, Lokesh, John]

For numbers, a.age – b.age gives ascending order and b.age – a.age gives descending order. For strings, localeCompare() compares alphabetically and handles case and accented letters correctly; the < operator compares character codes, so “Zoe” would come before “adam”. We prefer toSorted() when the original order is still needed elsewhere, for example when the same list is shown sorted in one place and unsorted in another.

5. Extracting, Summing and Grouping Objects

Three more operations come up often with object arrays: taking one property from each object, combining a property into a single value, and splitting the array into groups. The map() method does the first, reduce() does the second, and the ES2024 static method Object.groupBy() does the third.

// people = [Lokesh 37, Raj 35, John 40]

// 1. One property from each object
const names = people.map((p) => p.name);                // names = ["Lokesh", "Raj", "John"]

// 2. Sum a property
const totalAge = people.reduce((sum, p) => sum + p.age, 0);   // totalAge = 112

// 3. Group by a computed key (ES2024)
const groups = Object.groupBy(people, (p) => (p.age < 38 ? "young" : "senior"));
// groups = { young: [Lokesh, Raj], senior: [John] }

// 4. Index by name for fast lookups
const byName = new Map(people.map((p) => [p.name, p]));
const john = byName.get("John");                        // john = { name: "John", age: 40 }

The 0 at the end of reduce() is the starting value of sum; without it, the first Person object would become the starting value and the addition would not type-check. The type of groups is Partial<Record<“young” | “senior”, Person[]>>: every key is optional, because a group with no members is left out of the result. So groups.young has the type Person[] | undefined.

Option 4 turns the array into a Map keyed by name. A find() call scans the array from the start every time, while Map.get() goes straight to the entry. When we look up many objects by the same property, building the Map once is faster.

6. Copies, readonly and Updates Without Mutation

An array of objects holds references to the objects, not the objects themselves. Copying the array with […people] or slice() creates a new array that points to the same objects. Changing a property through the copy changes the original too. This is the most common source of “my original data changed” bugs with object arrays, for example in UI state where the old and new arrays must stay different.

// people = [Lokesh 37, Raj 35, John 40]

// 1. Update one object, return a new array
const updated = people.map((p) => (p.name === "Raj" ? { ...p, age: 36 } : p));
// updated[1].age = 36, people[1].age = 35

// 2. Replace or remove by index (ES2023)
const replaced = people.with(0, { name: "Amit", age: 30 });   // [Amit, Raj, John]
const without = people.toSpliced(1, 1);                 // [Lokesh, John]

// 3. A spread copy shares the same objects
const shallow = [...people];
shallow[0].age = 99;                                    // people[0].age = 99 too

// 4. structuredClone() copies the objects as well
const deep = structuredClone(people);
deep[1].age = 50;                                       // people[1].age stays 35

// 5. readonly blocks changes to the array, not to the objects
const fixed: readonly Person[] = people;
fixed[2].age = 41;                                      // compiles

Option 1 is the usual pattern for an update without mutation: map() returns a new array, the changed object is a new object, and the other objects are reused. The ES2023 methods with() and toSpliced() are the copying versions of people[0] = x and splice(). The global structuredClone() function makes a deep copy, so nested objects are copied too; it throws an error for functions, and copied class instances become plain objects without their methods.

A readonly Person[] type, also written ReadonlyArray<Person>, removes push(), pop(), splice(), sort() and index assignment from the type. It does not freeze the objects inside, so fixed[2].age = 41 still compiles. For read-only properties, we mark them in the interface with the readonly keyword or use Readonly<Person>[].

const fixed: readonly Person[] = people;
fixed.push({ name: "Amit", age: 30 });
fixed[0] = { name: "Amit", age: 30 };
src/err.ts(2,7): error TS2339: Property 'push' does not exist on type 'readonly Person[]'.
src/err.ts(3,1): error TS2542: Index signature in type 'readonly Person[]' only permits reading.

We use readonly arrays for function parameters that should not change the caller’s data. The toSorted(), with() and toSpliced() methods are available on readonly arrays, because they return new arrays.

7. Running the Array of Objects Examples

The code on GitHub has one file per section and a shared Person interface in src/person.ts. It targets TypeScript 7.0.2 with the ES2024 library, which includes toSorted(), findLast() and Object.groupBy(), and Node.js 22 is the oldest version it supports.

npm install
npm start

Running npm start builds the code with tsc first and then prints the value behind each snippet comment, section by section.

8. Conclusion

An array of objects in TypeScript becomes safe once the element type is declared, either as Person[] or Array<Person> with an interface or type alias. The compiler then catches missing and unknown properties. We search with find(), filter() and some(), sort with a compare function and toSorted(), and group with Object.groupBy(). The points that need care are index access that returns undefined, includes() comparing references, and copies that share the same objects. The TypeScript arrays guide covers arrays of primitive values, and creating objects from an interface shows more ways to build the objects themselves.

9. References

The TypeScript Handbook explains array and object types, and MDN documents each array method used above.

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.