How to Filter an Array of Objects in TypeScript

To filter an array of objects in TypeScript, pass filter() a function that tests a property: orders.filter(order => order.status === "shipped"). You get a new array of the same object type, the original is untouched, and the compiler checks every property name as you type it. This guide covers filtering by one property and by several conditions at once, search boxes, sets of selected ids, optional and nested properties, narrowing a union type so the result is the specific type you want, one generic helper for any property, and what to do when the list gets long.

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 and then node file.js. The output is the real output. Reference: Narrowing in the TypeScript Handbook.

Filter an array of objects by a property

The predicate receives one object at a time and keeps the ones it answers true for. Because the array is typed, order. autocompletes and a misspelled property is a compile error rather than a silent empty result:

interface Order {
  id: string;
  customer: string;
  city: string;
  total: number;
  status: "pending" | "shipped" | "cancelled";
  placed: string;                 // an ISO date from the API
  coupon?: string;                // optional: most orders have none
}

const orders: Order[] = [
  { id: "ORD-1001", customer: "Emma Johnson", city: "Austin", total: 249.99, status: "shipped", placed: "2026-03-14" },
  { id: "ORD-1002", customer: "Michael Brown", city: "Denver", total: 89.5, status: "pending", placed: "2026-04-02", coupon: "SPRING10" },
  { id: "ORD-1003", customer: "Olivia Davis", city: "Austin", total: 430, status: "shipped", placed: "2026-04-18" },
  { id: "ORD-1004", customer: "Noah Wilson", city: "Seattle", total: 15, status: "cancelled", placed: "2026-05-05", coupon: "WELCOME" },
];

// by a string property
const shipped = orders.filter(order => order.status === "shipped");
console.log(shipped.map(o => o.id));

// by a number property
console.log(orders.filter(order => order.total >= 100).map(o => `${o.id} $${o.total}`));

// the compiler checks the property names: order.stat would not compile
console.log(orders.filter(order => order.city === "Austin").length, "orders from Austin");

Output:

[ 'ORD-1001', 'ORD-1003' ]
[ 'ORD-1001 $249.99', 'ORD-1003 $430' ]
2 orders from Austin
Command Prompt showing TypeScript filtering an array of order objects by status, by total and by city, compiled with tsc and run with node
Same array, three filters: by status, by amount and by city.

Everything here is the ordinary filter() method; if you want its full behaviour, including filter() versus find() and the index argument, see our guide to filtering arrays in TypeScript.

Filter with multiple conditions

Combine tests with && for “all of these” and || for “any of these”. A search box is the same idea over two or three fields, lowercased on both sides so the match is case-insensitive, and returning everything when the box is empty:

interface Order {
  id: string;
  customer: string;
  city: string;
  total: number;
  status: "pending" | "shipped" | "cancelled";
  placed: string;                 // an ISO date from the API
  coupon?: string;                // optional: most orders have none
}

const orders: Order[] = [
  { id: "ORD-1001", customer: "Emma Johnson", city: "Austin", total: 249.99, status: "shipped", placed: "2026-03-14" },
  { id: "ORD-1002", customer: "Michael Brown", city: "Denver", total: 89.5, status: "pending", placed: "2026-04-02", coupon: "SPRING10" },
  { id: "ORD-1003", customer: "Olivia Davis", city: "Austin", total: 430, status: "shipped", placed: "2026-04-18" },
  { id: "ORD-1004", customer: "Noah Wilson", city: "Seattle", total: 15, status: "cancelled", placed: "2026-05-05", coupon: "WELCOME" },
];

// AND: every condition must hold
console.log(orders
  .filter(order => order.status !== "cancelled" && order.total >= 100 && order.city === "Austin")
  .map(o => o.id));

// OR: any of them
console.log(orders.filter(order => order.city === "Denver" || order.total > 400).map(o => o.id));

// a search box across two fields, case-insensitively
function search(term: string): Order[] {
  const needle = term.trim().toLowerCase();
  if (!needle) return orders;
  return orders.filter(order =>
    order.customer.toLowerCase().includes(needle) || order.city.toLowerCase().includes(needle)
  );
}

console.log(search("AUS").map(o => o.id));
console.log(search("olivia").map(o => o.customer));
console.log(search("   ").length, "orders when the box is empty");

Output:

[ 'ORD-1001', 'ORD-1003' ]
[ 'ORD-1002', 'ORD-1003' ]
[ 'ORD-1001', 'ORD-1003' ]
[ 'Olivia Davis' ]
4 orders when the box is empty
Command Prompt output of TypeScript filtering objects with AND and OR conditions and a case-insensitive search across customer and city
“AUS” matches the city, “olivia” matches the customer, and an empty box returns all four orders.

Filter by a set of selected values

Checkbox lists, multi-selects and “remove these rows” all come down to the same thing: a Set of the chosen values, then has() inside the predicate. It reads better than a long chain of || and it stays fast as the list grows. Dates that arrive as ISO strings can be compared directly, because "2026-04-02" >= "2026-04-01" is true as text, which saves converting them to Date objects:

interface Order {
  id: string;
  customer: string;
  city: string;
  total: number;
  status: "pending" | "shipped" | "cancelled";
  placed: string;                 // an ISO date from the API
  coupon?: string;                // optional: most orders have none
}

const orders: Order[] = [
  { id: "ORD-1001", customer: "Emma Johnson", city: "Austin", total: 249.99, status: "shipped", placed: "2026-03-14" },
  { id: "ORD-1002", customer: "Michael Brown", city: "Denver", total: 89.5, status: "pending", placed: "2026-04-02", coupon: "SPRING10" },
  { id: "ORD-1003", customer: "Olivia Davis", city: "Austin", total: 430, status: "shipped", placed: "2026-04-18" },
  { id: "ORD-1004", customer: "Noah Wilson", city: "Seattle", total: 15, status: "cancelled", placed: "2026-05-05", coupon: "WELCOME" },
];

// keep only the ids the user selected
const selected = new Set(["ORD-1001", "ORD-1004"]);
console.log(orders.filter(order => selected.has(order.id)).map(o => o.id));

// or drop them instead
console.log(orders.filter(order => !selected.has(order.id)).map(o => o.id));

// several allowed values for one property
const active = new Set<Order["status"]>(["pending", "shipped"]);
console.log(orders.filter(order => active.has(order.status)).map(o => `${o.id} ${o.status}`));

// a date range, comparing ISO strings directly
const from = "2026-04-01", to = "2026-04-30";
console.log(orders.filter(order => order.placed >= from && order.placed <= to).map(o => o.placed));

Output:

[ 'ORD-1001', 'ORD-1004' ]
[ 'ORD-1002', 'ORD-1003' ]
[ 'ORD-1001 shipped', 'ORD-1002 pending', 'ORD-1003 shipped' ]
[ '2026-04-02', '2026-04-18' ]

Optional and nested properties

Real API data has gaps. ?. stops the filter throwing when an object has no address at all, and it returns undefined, which is falsy, so the object is simply excluded:

interface Address {
  city: string;
  zip?: string;
}

interface Customer {
  name: string;
  address?: Address;
  tags?: string[];
}

const customers: Customer[] = [
  { name: "Emma", address: { city: "Austin", zip: "78701" }, tags: ["vip"] },
  { name: "Michael", address: { city: "Denver" } },
  { name: "Olivia" },
];

// optional chaining keeps the filter safe when the property is missing
console.log(customers.filter(c => c.address?.city === "Austin").map(c => c.name));
console.log(customers.filter(c => c.address?.zip !== undefined).map(c => c.name));
console.log(customers.filter(c => c.tags?.includes("vip")).map(c => c.name));

// without it, TypeScript stops you: c.address.city would be "possibly undefined"
console.log(customers.filter(c => (c.address?.city ?? "") !== "").length, "customers have a city");

Output:

[ 'Emma' ]
[ 'Emma' ]
[ 'Emma' ]
2 customers have a city

Without the question mark, c.address.city does not compile under strict, which is the compiler saving you from a runtime crash on the third customer.

Narrowing: getting Shipped[] instead of Order[]

This is the part that separates TypeScript from JavaScript. Filtering a union by its discriminant gives you the right objects at runtime, but the type stays the wide union, so the property that only exists on one member is still an error:

interface Pending {
  kind: "pending";
  id: string;
}

interface Shipped {
  kind: "shipped";
  id: string;
  trackingNumber: string;
}

type Order = Pending | Shipped;

const orders: Order[] = [
  { kind: "pending", id: "ORD-1002" },
  { kind: "shipped", id: "ORD-1001", trackingNumber: "1Z999" },
];

const shipped: Shipped[] = orders.filter(order => order.kind === "shipped");
console.log(shipped.map(o => o.trackingNumber));

tsc output:

filter_union_error.ts(19,7): error TS2322: Type 'Order[]' is not assignable to type 'Shipped[]'.
  Type 'Order' is not assignable to type 'Shipped'.
    Property 'trackingNumber' is missing in type 'Pending' but required in type 'Shipped'.
Command Prompt showing TypeScript error TS2322 because filtering a union array still produces the union type rather than the narrowed type
The filter worked; the type did not follow. trackingNumber is not on Pending.

A type predicate fixes it. (order): order is Shipped tells the compiler what survives, and the result is a real Shipped[]. TypeScript 5.5 and later infer this from the function body, so on a newer compiler the plain arrow function works too. The same trick removes undefined from an array of optional values:

interface Pending {
  kind: "pending";
  id: string;
}

interface Shipped {
  kind: "shipped";
  id: string;
  trackingNumber: string;
}

type Order = Pending | Shipped;

const orders: Order[] = [
  { kind: "pending", id: "ORD-1002" },
  { kind: "shipped", id: "ORD-1001", trackingNumber: "1Z999" },
];

// the type predicate tells the compiler what survived the filter
const isShipped = (order: Order): order is Shipped => order.kind === "shipped";

const shipped: Shipped[] = orders.filter(isShipped);
console.log(shipped.map(order => `${order.id} ${order.trackingNumber}`));

Output:

[ 'ORD-1001 1Z999' ]

One generic helper for any property

If you filter by different properties all over the codebase, K extends keyof T gives you a single type-safe helper: only real property names are accepted, and the value has to match that property’s type. The second function is the one you want behind a filter panel, where each control may or may not be set, and it pairs well with filtering by multiple properties:

interface Order {
  id: string;
  customer: string;
  city: string;
  total: number;
  status: "pending" | "shipped" | "cancelled";
  placed: string;                 // an ISO date from the API
  coupon?: string;                // optional: most orders have none
}

const orders: Order[] = [
  { id: "ORD-1001", customer: "Emma Johnson", city: "Austin", total: 249.99, status: "shipped", placed: "2026-03-14" },
  { id: "ORD-1002", customer: "Michael Brown", city: "Denver", total: 89.5, status: "pending", placed: "2026-04-02", coupon: "SPRING10" },
  { id: "ORD-1003", customer: "Olivia Davis", city: "Austin", total: 430, status: "shipped", placed: "2026-04-18" },
  { id: "ORD-1004", customer: "Noah Wilson", city: "Seattle", total: 15, status: "cancelled", placed: "2026-05-05", coupon: "WELCOME" },
];

// one helper for any property of any object array
function filterBy<T, K extends keyof T>(items: readonly T[], key: K, value: T[K]): T[] {
  return items.filter(item => item[key] === value);
}

console.log(filterBy(orders, "city", "Austin").map(o => o.id));
console.log(filterBy(orders, "status", "cancelled").map(o => o.id));

// a whole set of filters, skipping the ones the user left empty
type Filters = Partial<Pick<Order, "city" | "status">> & { minTotal?: number };

function applyFilters(items: readonly Order[], filters: Filters): Order[] {
  return items.filter(order =>
    (filters.city === undefined || order.city === filters.city) &&
    (filters.status === undefined || order.status === filters.status) &&
    (filters.minTotal === undefined || order.total >= filters.minTotal)
  );
}

console.log(applyFilters(orders, { city: "Austin" }).map(o => o.id));
console.log(applyFilters(orders, { status: "shipped", minTotal: 300 }).map(o => o.id));
console.log(applyFilters(orders, {}).length, "orders when nothing is selected");

Output:

[ 'ORD-1001', 'ORD-1003' ]
[ 'ORD-1004' ]
[ 'ORD-1001', 'ORD-1003' ]
[ 'ORD-1003' ]
4 orders when nothing is selected
Command Prompt output of a generic TypeScript filterBy function using keyof and an applyFilters function that skips unset filters
applyFilters({}) returns everything, which is exactly what an untouched filter panel should do.

When the array gets long, stop using includes()

filter() walks the array once, but if the predicate itself searches another array, you are doing a search inside a search. Over 50,000 rows against 5,000 selected ids, the difference is not subtle:

interface Row {
  id: number;
  city: string;
}

const rows: Row[] = Array.from({ length: 50_000 }, (_, i) => ({ id: i, city: i % 2 ? "Austin" : "Denver" }));
const wanted: number[] = Array.from({ length: 5_000 }, (_, i) => i * 7);
const wantedSet = new Set(wanted);

function time(label: string, fn: () => number): void {
  fn();                                   // warm-up run
  const start = performance.now();
  const kept = fn();
  console.log(label.padEnd(24), (performance.now() - start).toFixed(1).padStart(8), "ms", "kept", kept);
}

time("includes() on array", () => rows.filter(row => wanted.includes(row.id)).length);
time("Set.has()", () => rows.filter(row => wantedSet.has(row.id)).length);
Command Prompt output timing includes on an array against Set has while filtering fifty thousand objects in TypeScript
Same result, different cost: Set.has() is a lookup, includes() is a scan.

Build the Set once outside the filter, not inside the predicate, or you pay for it on every element. The same rule applies when collecting unique values from an array of objects.

More TypeScript array guides worth reading:

Frequently asked questions

How do I filter an array of objects in TypeScript?

Test a property inside filter(): orders.filter(o => o.status === "shipped"). You get a new array of the same object type.

How do I filter with more than one condition?

Combine the tests: o.status !== "cancelled" && o.total >= 100. Use || when any of the conditions is enough.

How do I filter objects by several allowed values?

Put the values in a Set and call has() in the predicate: selected.has(o.status). It is shorter and faster than chained ||.

How do I filter by a property that might be missing?

Use optional chaining: customers.filter(c => c.address?.city === "Austin"). Without ?. the code will not compile under strict mode.

Why is my filtered array still the union type?

A boolean predicate does not narrow. Write a type predicate, (o): o is Shipped => o.kind === "shipped", or use TypeScript 5.5 or later, which infers it.

How do I search an array of objects by text?

Lowercase both sides and use includes(): o.customer.toLowerCase().includes(term.toLowerCase()), testing each field you want to search.

Does filter() change the original array of objects?

No. It returns a new array, though the objects inside it are the same references, so editing one still affects both arrays.