How to Work With Arrays of Objects in TypeScript

Recently, I was working on a TypeScript project where I needed to manage a collection of user profiles with various properties. The challenge was to ensure type safety while still maintaining flexibility. The solution? TypeScript arrays of objects.

In this tutorial, I’ll cover everything you need to know about working with arrays of objects in TypeScript, including declaration, manipulation, and best practices.

So let’s dive in!

Declare Arrays of Objects in TypeScript

Working with arrays of objects in TypeScript is one of the most common tasks you’ll encounter in frontend and backend development. When I first started with TypeScript, I realized that proper typing for object arrays saves countless hours of debugging.

Here are the ways you can declare an array of objects in TypeScript:

Using Type Interfaces for Object Arrays

The most common and recommended approach is to define an interface for your objects first:

interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
}

// Creating an array of User objects
const users: User[] = [
  { id: 1, name: 'John Smith', email: 'john@example.com', isActive: true },
  { id: 2, name: 'Sarah Johnson', email: 'sarah@example.com', isActive: false }
];

In this example, I’ve created a User interface that defines the structure of each object in our array. The User[] syntax tells TypeScript that we’re creating an array where each element must conform to the User interface.

I use this approach in most of my projects because it gives me clear type definitions that can be reused throughout the codebase.

Using Type Aliases

Another approach is to use type aliases:

type Product = {
  id: number;
  title: string;
  price: number;
  inStock: boolean;
};

// Creating an array of Product objects
const products: Product[] = [
  { id: 101, title: 'iPhone 13', price: 999, inStock: true },
  { id: 102, title: 'MacBook Pro', price: 1999, inStock: false }
];

Type aliases work similarly to interfaces but use a different syntax. For simple object types, they function almost identically to interfaces. The choice between them often comes down to personal preference.

Inline Type Declarations

For simpler cases, you can define the type inline:

const states: { name: string; abbreviation: string; population: number }[] = [
  { name: 'California', abbreviation: 'CA', population: 39.5 },
  { name: 'Texas', abbreviation: 'TX', population: 29.1 },
  { name: 'Florida', abbreviation: 'FL', population: 21.5 }
];

Here, I’m defining the object structure directly in the variable declaration. This approach works well for quick, one-off uses but becomes unwieldy for complex objects or when you need to reuse the type.

Arrays of Objects in TypeScript

Check out How to Iterate Over a JSON Object in TypeScript

Manipulate TypeScript Arrays of Objects

Once you’ve declared your array of objects, you’ll need to manipulate it. Here are some common operations:

Adding Elements to an Object Array

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

const tasks: Task[] = [
  { id: 1, title: 'Complete TypeScript tutorial', completed: false }
];

// Adding a new task at the end
tasks.push({ id: 2, title: 'Review PR', completed: false });

// Adding at the beginning
tasks.unshift({ id: 0, title: 'Check emails', completed: true });

The push() method adds elements to the end of an array, while unshift() adds them to the beginning. Notice how TypeScript ensures that the objects we’re adding match the Task interface.

Removing Elements from an Object Array

// Remove the last task
const lastTask = tasks.pop();

// Remove the first task
const firstTask = tasks.shift();

// Remove a specific task by filtering
const filteredTasks = tasks.filter(task => task.id !== 1);

The pop() method removes the last element, shift() removes the first element, and filter() creates a new array without the elements that don’t pass the test function. The filter() method is particularly useful when you want to remove elements based on certain conditions.

Finding Elements in an Object Array

// Find a specific task
const task = tasks.find(task => task.id === 2);

// Check if a task exists
const taskExists = tasks.some(task => task.title.includes('TypeScript'));

// Get the index of a specific task
const taskIndex = tasks.findIndex(task => task.id === 1);

The find() method returns the first element that satisfies the testing function, some() checks if at least one element meets the condition, and findIndex() returns the index of the first element that meets the condition.

Check out How to Convert TypeScript Objects to JSON

Advanced Techniques for Object Arrays in TypeScript

Working with Optional Properties

In real-world scenarios, you often need to work with objects that might not have all properties:

interface Customer {
  id: number;
  name: string;
  email: string;
  address?: {
    street: string;
    city: string;
    state: string;
    zip: string;
  };
}

const customers: Customer[] = [
  { id: 1, name: 'John Doe', email: 'john@example.com' },
  { 
    id: 2, 
    name: 'Jane Smith', 
    email: 'jane@example.com',
    address: {
      street: '123 Main St',
      city: 'New York',
      state: 'NY',
      zip: '10001'
    }
  }
];

The question mark (?) after address indicates that this property is optional. This means some customers might have an address, while others might not. This flexibility is crucial when working with real data.

Using Union Types in Object Arrays

Sometimes you need an array that can contain different types of objects:

type Shape = 
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number };

const shapes: Shape[] = [
  { kind: 'circle', radius: 5 },
  { kind: 'rectangle', width: 10, height: 20 }
];

// Type narrowing when working with the array
shapes.forEach(shape => {
  if (shape.kind === 'circle') {
    console.log(`Circle with radius ${shape.radius}`);
  } else {
    console.log(`Rectangle with dimensions ${shape.width}x${shape.height}`);
  }
});

This is a powerful TypeScript feature called discriminated unions. The kind property acts as a discriminator that helps TypeScript understand which type of shape we’re working with. Notice how TypeScript narrows down the type in the if-else blocks.

Create Readonly Object Arrays

When you want to prevent modifications to your array:

const USStates: ReadonlyArray<{ name: string; capital: string }> = [
  { name: 'New York', capital: 'Albany' },
  { name: 'California', capital: 'Sacramento' },
  { name: 'Texas', capital: 'Austin' }
];

// This would cause a TypeScript error
// USStates.push({ name: 'Florida', capital: 'Tallahassee' });

ReadonlyArray<T> is a built-in TypeScript type that prevents mutations to the array. This is useful when you want to ensure that certain data remains constant throughout your application.

Check out How to Import JSON Files in TypeScript?

Common Patterns and Best Practices

Transforming Object Arrays

interface Employee {
  id: number;
  name: string;
  department: string;
  salary: number;
}

const employees: Employee[] = [
  { id: 1, name: 'Alice Johnson', department: 'Engineering', salary: 120000 },
  { id: 2, name: 'Bob Smith', department: 'Marketing', salary: 95000 },
  { id: 3, name: 'Carol Davis', department: 'Engineering', salary: 110000 }
];

// Map to get just the names
const employeeNames: string[] = employees.map(emp => emp.name);

// Calculate total salary
const totalSalary = employees.reduce((sum, emp) => sum + emp.salary, 0);

// Group by department
const byDepartment = employees.reduce((acc, emp) => {
  acc[emp.department] = acc[emp.department] || [];
  acc[emp.department].push(emp);
  return acc;
}, {} as Record<string, Employee[]>);

The map() method creates a new array by transforming each element. Here, we’re extracting just the names from our employee objects.

The reduce() method is incredibly versatile. In the first example, we’re using it to sum up all salaries. In the second example, we’re using it to group employees by department.

Sorting Arrays of Objects

// Sort employees by salary (highest first)
const sortedBySalary = [...employees].sort((a, b) => b.salary - a.salary);

// Sort by name
const sortedByName = [...employees].sort((a, b) => a.name.localeCompare(b.name));

The sort() method allows us to arrange our array elements based on different criteria. Note that I’m using the spread operator (...) to create a copy of the array before sorting, as sort() modifies the original array.

The localeCompare() method is especially useful for sorting strings in a locale-aware manner.

Check out TypeScript Enum with Multiple Values

Handling Arrays of Objects in Functions

When working with functions that accept or return arrays of objects, proper typing is crucial:

interface Order {
  id: string;
  customerName: string;
  items: { productId: string; quantity: number; price: number }[];
  total: number;
  date: Date;
}

// Function that returns an array of objects
function getRecentOrders(days: number): Order[] {
  // Implementation...
  return [
    {
      id: 'ORD-12345',
      customerName: 'Michael Johnson',
      items: [{ productId: 'P-100', quantity: 2, price: 29.99 }],
      total: 59.98,
      date: new Date('2023-05-15')
    }
  ];
}

// Function that filters an array of objects
function filterOrdersByTotal(orders: Order[], minTotal: number): Order[] {
  return orders.filter(order => order.total >= minTotal);
}

// Function with a callback that processes objects
function processOrders(
  orders: Order[], 
  processor: (order: Order) => void
): void {
  orders.forEach(processor);
}

These examples show different ways of working with object arrays in functions:

  • getRecentOrders() returns an array of Order objects
  • filterOrdersByTotal() takes an array of orders and returns a filtered array
  • processOrders() takes an array and a callback function that processes each order

By properly typing these functions, we ensure that they’ll be used correctly throughout our codebase.

Read How to Push Objects into an Array in TypeScript?

Error Handling with Object Arrays

When working with arrays of objects, especially from external sources like APIs, proper error handling is essential:

interface ApiResponse<T> {
  data: T[];
  error: string | null;
  status: number;
}

// Function to fetch users from an API
async function fetchUsers(): Promise<ApiResponse<User>> {
  try {
    const response = await fetch('https://api.example.com/users');
    const data = await response.json();

    return {
      data: data as User[],
      error: null,
      status: response.status
    };
  } catch (error) {
    return {
      data: [],
      error: error instanceof Error ? error.message : 'Unknown error',
      status: 500
    };
  }
}

// Using the function
async function displayUsers() {
  const result = await fetchUsers();

  if (result.error) {
    console.error(`Failed to fetch users: ${result.error}`);
    return;
  }

  // Process the users array
  result.data.forEach(user => {
    console.log(`User: ${user.name}, Email: ${user.email}`);
  });
}

In this example, I’ve created a generic ApiResponse<T> interface that can wrap any array of objects. The fetchUsers() function handles potential errors and provides a consistent response structure, making error handling more predictable throughout the application.

Check out Get String Between 2 Characters In TypeScript

Type Guards for TypeScript Object Arrays

When working with complex object arrays, type guards help ensure type safety:

interface AdminUser {
  id: number;
  name: string;
  role: 'admin';
  permissions: string[];
}

interface RegularUser {
  id: number;
  name: string;
  role: 'user';
  subscription: 'free' | 'premium';
}

type SystemUser = AdminUser | RegularUser;

const users: SystemUser[] = [
  { id: 1, name: 'Alice', role: 'admin', permissions: ['read', 'write', 'delete'] },
  { id: 2, name: 'Bob', role: 'user', subscription: 'premium' }
];

// Type guard function
function isAdminUser(user: SystemUser): user is AdminUser {
  return user.role === 'admin';
}

// Using the type guard
users.forEach(user => {
  if (isAdminUser(user)) {
    // TypeScript knows this is an AdminUser
    console.log(`Admin ${user.name} has permissions: ${user.permissions.join(', ')}`);
  } else {
    // TypeScript knows this is a RegularUser
    console.log(`User ${user.name} has a ${user.subscription} subscription`);
  }
});

Here, I’ve created a type guard isAdminUser() that helps TypeScript understand which specific type we’re working with. This is especially useful when dealing with arrays containing different object types.

Performance Considerations

When working with large arrays of objects, performance becomes important:

interface Transaction {
  id: string;
  amount: number;
  date: Date;
  category: string;
}

const transactions: Transaction[] = [
  /* Imagine thousands of transactions here */
];

// INEFFICIENT way to find transactions by category
function getTransactionsByCategory(category: string): Transaction[] {
  return transactions.filter(t => t.category === category);
}

// MORE EFFICIENT approach using an index
const transactionsByCategory: Record<string, Transaction[]> = {};

// Build the index once
transactions.forEach(transaction => {
  if (!transactionsByCategory[transaction.category]) {
    transactionsByCategory[transaction.category] = [];
  }
  transactionsByCategory[transaction.category].push(transaction);
});

// Fast lookup by category
function getTransactionsByCategoryFast(category: string): Transaction[] {
  return transactionsByCategory[category] || [];
}

In this example, I’ve demonstrated how creating an index can dramatically improve performance when querying a large array of objects frequently. This pattern is similar to database indexing and can significantly improve the responsiveness of your application.

Immutable Operations on Object Arrays

When working with state management libraries or frameworks like React, immutable operations are preferred:

interface TodoItem {
  id: number;
  text: string;
  completed: boolean;
}

const todos: TodoItem[] = [
  { id: 1, text: 'Learn TypeScript', completed: false },
  { id: 2, text: 'Build a project', completed: false },
  { id: 3, text: 'Write documentation', completed: true }
];

// Immutably toggle a todo's completed status
function toggleTodo(todos: TodoItem[], id: number): TodoItem[] {
  return todos.map(todo => 
    todo.id === id 
      ? { ...todo, completed: !todo.completed }
      : todo
  );
}

// Usage
const updatedTodos = toggleTodo(todos, 1);
console.log(updatedTodos);
// Original array remains unchanged
console.log(todos);

In this example, I’m using the map() method along with the spread operator to create a new array with one modified object, rather than modifying the original array. This immutable approach helps prevent bugs in applications where state changes need to be predictable and trackable.

Nested Object Arrays

Working with nested arrays of objects is a common scenario in complex applications:

interface Department {
  id: number;
  name: string;
  employees: Employee[];
}

const departments: Department[] = [
  {
    id: 1,
    name: 'Engineering',
    employees: [
      { id: 101, name: 'Alice', salary: 120000 },
      { id: 102, name: 'Bob', salary: 110000 }
    ]
  },
  {
    id: 2,
    name: 'Marketing',
    employees: [
      { id: 201, name: 'Carol', salary: 95000 }
    ]
  }
];

// Finding an employee across all departments
function findEmployee(employeeId: number): Employee | undefined {
  for (const dept of departments) {
    const employee = dept.employees.find(emp => emp.id === employeeId);
    if (employee) return employee;
  }
  return undefined;
}

// Flattening nested arrays
const allEmployees = departments.flatMap(dept => dept.employees);
console.log(allEmployees);

In this example, I’m working with a nested structure where each department has its own array of employees. The flatMap() method is particularly useful for transforming and flattening nested arrays in a single operation.

In this tutorial, I explained how to work with an array of objects in TypeScript. I hope you found this article helpful! If you have any questions about working with arrays of objects in TypeScript, feel free to leave them in the comments below.

You may also like the following tutorials:

Power Apps functions free pdf

30 Power Apps Functions

This free guide walks you through the 30 most-used Power Apps functions with real business examples, exact syntax, and results you can see.

Download User registration canvas app

DOWNLOAD USER REGISTRATION POWER APPS CANVAS APP

Download a fully functional Power Apps Canvas App (with Power Automate): User Registration App