How to Filter Arrays in TypeScript

To filter an array in TypeScript, call filter() with a function that returns true for the elements you want to keep: temperatures.filter(temp => temp > 80). It returns a new array and never changes the original, so the result of filtering a number[] is a number[]. The one place TypeScript surprises people is removing undefined or null, where the values disappear at runtime but the type does not change unless you use a type predicate. Everything below was compiled and run, including that case.

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: Array.prototype.filter() on MDN.

Filter arrays in TypeScript

The function you pass is called a predicate: it runs once per element and keeps the ones it answers true for. Nothing is removed from the original array:

const temperatures: number[] = [68, 102, 55, 91, 47];

const hot: number[] = temperatures.filter(temp => temp > 80);
console.log(hot, "original:", temperatures);

const cities: string[] = ["Austin", "Denver", "Seattle", "Atlanta"];
console.log(cities.filter(city => city.startsWith("A")));

console.log(cities.filter(city => city === "Chicago"));      // no match: an empty array

Output:

[ 102, 91 ] original: [ 68, 102, 55, 91, 47 ]
[ 'Austin', 'Atlanta' ]
[]
Command Prompt showing a TypeScript file compiled with tsc and run with node, filtering numbers above eighty and city names starting with A
filter() builds a new array; the original is untouched, and no match means [].
What you wantUseYou get back
Every element that matchesarr.filter(x => ...)A new array, possibly empty
The first match onlyarr.find(x => ...)The element, or undefined
Just a yes or noarr.some(x => ...)true or false
The position of a matcharr.findIndex(x => ...)The index, or -1
Matches with a narrower typearr.filter((x): x is T => ...)T[]
Matching and changing in one passarr.reduce(...) or flatMap(...)Whatever you build

The predicate gets the index and the array too

The second argument is the index and the third is the array being filtered, which is handy for every-other-element rules or for comparing against something you worked out from the whole array. Because filter() creates a new array rather than editing the old one, it also works on a readonly array, unlike sort() or splice():

const readings: number[] = [12, 45, 7, 88, 23, 61];

// the predicate also receives the index and the whole array
console.log(readings.filter((value, index) => index % 2 === 0));

const average = readings.reduce((sum, n) => sum + n, 0) / readings.length;
console.log("average:", average.toFixed(1), readings.filter(value => value > average));

// filter() never changes the array, so a readonly array is fine
const limits: readonly number[] = [10, 20, 30];
const big: number[] = limits.filter(limit => limit >= 20);
console.log(big, limits);

Output:

[ 12, 7, 23 ]
average: 39.3 [ 45, 88, 61 ]
[ 20, 30 ] [ 10, 20, 30 ]

Filter an array of objects

With objects, the predicate tests a property, and TypeScript checks the property names as you type them. Combine conditions with && and ||, and lowercase both sides for a case-insensitive search. There is a deeper guide to filtering an array of objects and one for filtering by multiple properties:

interface Order {
  id: string;
  customer: string;
  total: number;
  status: "pending" | "shipped" | "cancelled";
}

const orders: Order[] = [
  { id: "ORD-1001", customer: "Emma Johnson", total: 249.99, status: "shipped" },
  { id: "ORD-1002", customer: "Michael Brown", total: 89.5, status: "pending" },
  { id: "ORD-1003", customer: "Olivia Davis", total: 430, status: "shipped" },
  { id: "ORD-1004", customer: "Noah Wilson", total: 15, status: "cancelled" },
];

console.log(orders.filter(o => o.status === "shipped").map(o => o.id));

console.log(orders.filter(o => o.status !== "cancelled" && o.total < 250).map(o => o.id));

const term = "OLIVIA";
console.log(orders.filter(o => o.customer.toLowerCase().includes(term.toLowerCase())).map(o => o.customer));

Output:

[ 'ORD-1001', 'ORD-1003' ]
[ 'ORD-1001', 'ORD-1002' ]
[ 'Olivia Davis' ]

Filtering out undefined does not change the type

This is the TypeScript-specific part, and the reason plain JavaScript tutorials cannot help you. On TypeScript 5.2, neither a plain comparison nor filter(Boolean) tells the compiler that the empty values are gone:

const rawEmails: (string | undefined)[] = ["emma@example.com", undefined, "noah@example.com"];

const emails: string[] = rawEmails.filter(email => email !== undefined);
const shorter: string[] = rawEmails.filter(Boolean);        // does not narrow either

console.log(emails, shorter);

tsc output:

filter_narrowing.ts(3,7): error TS2322: Type '(string | undefined)[]' is not assignable to type 'string[]'.
  Type 'string | undefined' is not assignable to type 'string'.
    Type 'undefined' is not assignable to type 'string'.
filter_narrowing.ts(4,7): error TS2322: Type '(string | undefined)[]' is not assignable to type 'string[]'.
Command Prompt showing two TypeScript TS2322 errors because filtering undefined values still returns an array typed string or undefined
The values are gone at runtime; the type still says (string | undefined)[].

The fix is a type predicate: (email): email is string. Write it once as a reusable isPresent helper and every array in your codebase can use it. TypeScript 5.5 and later infer this from the function body, so on a newer compiler the first version above works on its own. For this one job there is also a focused guide to removing undefined values from an array:

const rawEmails: (string | undefined)[] = ["emma@example.com", undefined, "noah@example.com"];

// a type predicate: "email is string" tells the compiler what comes back
const emails: string[] = rawEmails.filter((email): email is string => email !== undefined);
console.log(emails.map(e => e.toUpperCase()));

// the same idea, reusable for any type
function isPresent<T>(value: T | null | undefined): value is T {
  return value !== null && value !== undefined;
}

const totals: (number | null)[] = [249.99, null, 430];
const clean: number[] = totals.filter(isPresent);
console.log(clean, clean.reduce((sum, n) => sum + n, 0).toFixed(2));

Output:

[ 'EMMA@EXAMPLE.COM', 'NOAH@EXAMPLE.COM' ]
[ 249.99, 430 ] 679.99
Command Prompt output showing a TypeScript type predicate used with filter so the result is typed as string array and a generic isPresent helper
With the predicate, the result is a real string[], so toUpperCase() compiles.

Narrow a union type with filter()

The same trick splits a union. Filtering a Vehicle[] with a predicate that returns vehicle is Car gives you a Car[], so the compiler lets you reach for properties that only cars have:

interface Car {
  kind: "car";
  plate: string;
}

interface Bike {
  kind: "bike";
  frame: string;
}

type Vehicle = Car | Bike;

const fleet: Vehicle[] = [
  { kind: "car", plate: "TX-4471" },
  { kind: "bike", frame: "alloy" },
  { kind: "car", plate: "CO-8820" },
];

const isCar = (vehicle: Vehicle): vehicle is Car => vehicle.kind === "car";

const cars: Car[] = fleet.filter(isCar);
console.log(cars.map(car => car.plate));        // .plate only exists on Car

Output:

[ 'TX-4471', 'CO-8820' ]

filter() vs find(), some() and findIndex()

Reach for filter() when you want all the matches. If you only need the first one, find() stops as soon as it hits it, and some() answers a yes-or-no question without building an array at all. Checking whether an array contains a value covers that side in more detail:

const ids: string[] = ["ORD-1001", "ORD-1002", "ORD-1003"];

console.log(ids.filter(id => id.endsWith("2")));        // every match, as an array
console.log(ids.find(id => id.endsWith("2")));          // the first match, or undefined
console.log(ids.find(id => id.endsWith("9")));          // no match: undefined
console.log(ids.some(id => id.endsWith("2")));          // is there at least one?
console.log(ids.filter(id => id.endsWith("9")).length === 0, "no matches");

Output:

[ 'ORD-1002' ]
ORD-1002
undefined
true
true no matches

Two things filter() does that surprise people

It skips holes in a sparse array, so the result can be shorter than the length suggests, and filter(Boolean) removes every falsy value, which includes 0, "" and NaN, not just null and undefined. If 0 is a real value in your data, that line quietly loses it. Use an explicit test instead, as the guide to removing empty strings does:

const readings: number[] = [10, 20, 30];
delete readings[1];                                  // a hole, length stays 3

console.log(readings.length, readings.filter(() => true), readings.filter(() => true).length);

const mixed: (string | number | null)[] = ["a", 0, null, "", 42, NaN];
console.log(mixed.filter(Boolean));                  // drops 0, null, "" and NaN as well
console.log(mixed.filter(value => value !== null));  // exactly what you asked for

Output:

3 [ 10, 30 ] 2
[ 'a', 42 ]
[ 'a', 0, '', 42, NaN ]
Command Prompt output showing TypeScript filter skipping a hole in a sparse array and filter Boolean also dropping zero empty string and NaN
filter(Boolean) dropped 0, "" and NaN along with null.

Does chaining filter() and map() cost anything?

Chaining reads beautifully and walks the data more than once, building an intermediate array each time. Over a million records, against a single reduce() and a plain loop over the array:

const orders: number[] = Array.from({ length: 1_000_000 }, (_, i) => i);

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

time("filter().map().reduce()", () =>
  orders.filter(n => n % 2 === 0).map(n => n * 2).reduce((sum, n) => sum + n, 0));

time("one reduce()", () =>
  orders.reduce((sum, n) => (n % 2 === 0 ? sum + n * 2 : sum), 0));

time("plain for loop", () => {
  let sum = 0;
  for (let i = 0; i < orders.length; i++) if (orders[i] % 2 === 0) sum += orders[i] * 2;
  return sum;
});
Command Prompt output timing a filter map reduce chain against a single reduce and a plain for loop over one million numbers in TypeScript
One run over 1,000,000 numbers, each timed after a warm-up.

For the list sizes most applications handle, the chain is the right choice: it says what you mean in one line. Collapse it into a single pass only when the array is genuinely large and the work shows up in a profile.

More TypeScript array guides to read next:

Frequently asked questions

How do I filter an array in TypeScript?

Call filter() with a predicate: numbers.filter(n => n > 10). It returns a new array with the elements the predicate answered true for.

Does filter() change the original array?

No. It always returns a new array, which is why it also works on readonly arrays, unlike sort() or splice().

How do I filter an array of objects in TypeScript?

Test a property: orders.filter(o => o.status === "shipped"). Combine conditions with &&, and the compiler checks every property name.

Why is my filtered array still typed string | undefined?

Because a plain predicate returns boolean. Use a type predicate, (v): v is string => v !== undefined. TypeScript 5.5 and later work it out for you.

What is the difference between filter() and find()?

filter() returns every match as an array. find() returns the first matching element, or undefined, and stops there.

Is filter(Boolean) safe to use?

Only when no valid value is falsy. It also removes 0, "" and NaN, and it does not narrow the type in TypeScript 5.2.

What does filter() return when nothing matches?

An empty array, never undefined, so you can safely call .length or .map() on the result.