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, such as 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.

We use arrays of objects for most list data in an app, such as the users in an admin table or the rows that a REST API returns as JSON.

The following example declares a Person array and runs the common operations on it, compiled with TypeScript 7.0.2 (strict mode, ES2024 library) and run on Node.js 22.

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
}

Notice that almost every operation takes a callback that reads one property, such as p.name or p.age. In the comments, a list such as [Lokesh, John] is short for the matching Person objects.

Next, we declare the array type in several ways and change the objects in it. After that, we find, sort and group objects by a property, and we look at the bugs that come from index access and from copies that share the same 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, and an inline type for 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. When one object misses a required property and another has a property that the interface does not declare, tsc reports both errors.

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. In that type, typeof team gives 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 items and remove items work the same for objects as for numbers or strings, and each of them changes the original array. For example, a team page adds a new member with push() and removes the member who left with splice().

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 and the second is the number of elements to delete, and all arguments after them are inserted. 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, as we will see in section 6.

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 part of strict, and 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. For example, a support dashboard finds one customer by email and lists all open tickets of that customer. The method we call decides how many results we get.

  • The find() method returns the first match.
  • The findLast() method (ES2023) returns the last match.
  • The filter() method returns all matches in a new array.
  • The some() and every() methods return true or false.
// 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, which for objects 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. It returns a negative number when a comes first, or a positive number when b comes first, and a result of 0 means the order of the two does not matter.

The ES2023 method toSorted() returns a new sorted array, whereas 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

Object arrays often need one value from each object or a summary of all objects. For example, an invoice page sums the amounts of all lines and groups the lines by tax rate. Each of these operations has its own method.

  • The map() method takes one property from each object.
  • The reduce() method combines one property of all objects into a single value, such as a total.
  • The ES2024 static method Object.groupBy() splits the array into groups by a key.
// 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, whereas Map.get() finds the entry by its key without a scan. 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, so changing a property through the copy changes the original too. Shared objects are 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 common pattern for an update without mutation. The map() method returns a new array in which only the changed object is new, 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 copying methods, such as toSorted(), with() and toSpliced(), are still available on readonly arrays, because they return new arrays and leave the original alone.

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 newer methods such as toSorted() 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 from each snippet comment, section by section.

8. Conclusion

An array of objects in TypeScript becomes safe once we declare the element type, either as Person[] or Array<Person>, with an interface or a type alias. From then on, the compiler catches missing and unknown properties in every object we add.

We search the array with find() and filter(), and we sort it with a compare function passed to toSorted(). Most bugs with object arrays come from copies that share the same objects or from includes() comparing references. Index access needs care too, because people[5] has the type Person even when the value is undefined.

The same methods work on TypeScript arrays of strings and numbers, and the objects themselves can come from any of the ways to create an object from an interface.

9. References

The TypeScript Handbook explains array and object types, and MDN documents each array method from the examples.

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.