Episode 1 — Fundamentals / 1.22 — Array Methods

1.22.b — filter()

In one sentence: filter() tests every element against a condition and returns a new array containing only the elements that passed.

Navigation: ← 1.22.a — map() · 1.22.c — reduce() →


1. What filter() does

filter() iterates through every element, passes it to your callback, and keeps the element if the callback returns a truthy value. Elements whose callback returns falsy are excluded.

const nums = [1, 2, 3, 4, 5, 6];
const evens = nums.filter(n => n % 2 === 0);

console.log(evens); // [2, 4, 6]
console.log(nums);  // [1, 2, 3, 4, 5, 6]  ← unchanged

2. Syntax

const newArray = arr.filter(callback(element, index, array));
ParameterDescription
elementThe current element being tested
index(optional) The index of the current element
array(optional) The original array filter was called on
ReturnA new array containing only elements where callback returned truthy
const words = ["spray", "limit", "elite", "destruction", "present"];

// Keep words longer than 5 characters
const long = words.filter((word, index) => {
  console.log(`Testing index ${index}: "${word}" (length ${word.length})`);
  return word.length > 5;
});

console.log(long); // ["destruction", "present"]

3. Returns empty array if nothing passes

filter never returns null or undefined. If no element passes the test, you get an empty array. This is safe to chain on.

const nums = [1, 2, 3];
const big = nums.filter(n => n > 100);

console.log(big);        // []
console.log(big.length); // 0
console.log(Array.isArray(big)); // true

// Safe to chain — no "cannot read property of null" errors
const result = nums.filter(n => n > 100).map(n => n * 2);
console.log(result); // []

4. Does NOT mutate the original

Like map, filter is a non-mutating method:

const scores = [85, 42, 93, 67, 55, 78];
const passing = scores.filter(s => s >= 60);

console.log(scores);  // [85, 42, 93, 67, 55, 78]  ← untouched
console.log(passing); // [85, 93, 67, 78]           ← new array

5. Truthy vs falsy in filter callbacks

The callback does not have to return strictly true or false. Any truthy value keeps the element; any falsy value removes it.

Truthy (keeps element)Falsy (removes element)
true, 1, "hello", {}, []false, 0, "", null, undefined, NaN
const mixed = [0, 1, "", "hello", null, undefined, false, 42, NaN];

// Keep only truthy values
const truthy = mixed.filter(val => val);
console.log(truthy); // [1, "hello", 42]

6. Filtering objects by property

This is the most common real-world use:

const students = [
  { name: "Alice", grade: 92, active: true },
  { name: "Bob", grade: 45, active: true },
  { name: "Carol", grade: 78, active: false },
  { name: "Dave", grade: 88, active: true },
  { name: "Eve", grade: 34, active: true },
];

// Students who passed (grade >= 60) AND are active
const passingActive = students.filter(s => s.grade >= 60 && s.active);

console.log(passingActive);
// [
//   { name: "Alice", grade: 92, active: true },
//   { name: "Dave", grade: 88, active: true },
// ]

Filtering by multiple conditions

const products = [
  { name: "Laptop", price: 999, category: "electronics", inStock: true },
  { name: "Shirt", price: 29, category: "clothing", inStock: true },
  { name: "Phone", price: 699, category: "electronics", inStock: false },
  { name: "Book", price: 15, category: "books", inStock: true },
  { name: "Headphones", price: 149, category: "electronics", inStock: true },
];

// Affordable electronics that are in stock
const affordable = products.filter(p =>
  p.category === "electronics" &&
  p.price < 500 &&
  p.inStock
);

console.log(affordable);
// [{ name: "Headphones", price: 149, category: "electronics", inStock: true }]

7. Chaining: filter(...).map(...)

The most common chain pattern is filter first (reduce the set), then map (transform the survivors):

const employees = [
  { name: "Alice", department: "Engineering", salary: 95000 },
  { name: "Bob", department: "Marketing", salary: 65000 },
  { name: "Carol", department: "Engineering", salary: 105000 },
  { name: "Dave", department: "Engineering", salary: 88000 },
  { name: "Eve", department: "Marketing", salary: 72000 },
];

// Get formatted names of engineers earning >= 90k
const seniorEngineers = employees
  .filter(e => e.department === "Engineering" && e.salary >= 90000)
  .map(e => `${e.name} ($${e.salary.toLocaleString()})`);

console.log(seniorEngineers);
// ["Alice ($95,000)", "Carol ($105,000)"]

Why filter before map? Fewer elements to transform = better performance and clearer intent.


8. Removing falsy values: filter(Boolean)

A very common JavaScript pattern:

const messy = ["Alice", "", null, "Bob", undefined, "Carol", 0, false, "Dave"];

const clean = messy.filter(Boolean);
console.log(clean); // ["Alice", "Bob", "Carol", "Dave"]

How it works: Boolean is a constructor function. When called as Boolean(value), it returns true for truthy values, false for falsy. filter uses this as its callback.

// These are equivalent:
messy.filter(Boolean);
messy.filter(val => Boolean(val));
messy.filter(val => !!val);

Practical use: cleaning up split results

const csv = "Alice,,Bob,,Carol";
const names = csv.split(",").filter(Boolean);
console.log(names); // ["Alice", "Bob", "Carol"]

Cleaning up optional fields

const parts = [user.firstName, user.middleName, user.lastName].filter(Boolean);
const fullName = parts.join(" ");
// If middleName is undefined: "Alice Smith"
// If middleName is "Marie": "Alice Marie Smith"

9. Using the index parameter

// Keep every other element (even indices)
const letters = ["a", "b", "c", "d", "e", "f"];
const everyOther = letters.filter((_, index) => index % 2 === 0);
console.log(everyOther); // ["a", "c", "e"]

Remove duplicates (keeping first occurrence)

const nums = [1, 3, 5, 3, 1, 5, 7];
const unique = nums.filter((val, index, arr) => arr.indexOf(val) === index);
console.log(unique); // [1, 3, 5, 7]

(For large arrays, prefer [...new Set(arr)] for performance.)


10. Real-world examples

Search functionality

const contacts = [
  { name: "Alice Johnson", email: "alice@example.com" },
  { name: "Bob Smith", email: "bob@example.com" },
  { name: "Carol Johnson", email: "carol@example.com" },
  { name: "Dave Williams", email: "dave@example.com" },
];

function searchContacts(query) {
  const lower = query.toLowerCase();
  return contacts.filter(c =>
    c.name.toLowerCase().includes(lower) ||
    c.email.toLowerCase().includes(lower)
  );
}

console.log(searchContacts("johnson"));
// [{ name: "Alice Johnson", ... }, { name: "Carol Johnson", ... }]

console.log(searchContacts("bob"));
// [{ name: "Bob Smith", ... }]

Remove completed todos

const todos = [
  { id: 1, text: "Learn map", done: true },
  { id: 2, text: "Learn filter", done: false },
  { id: 3, text: "Learn reduce", done: true },
  { id: 4, text: "Practice chaining", done: false },
];

const pending = todos.filter(todo => !todo.done);
console.log(pending);
// [
//   { id: 2, text: "Learn filter", done: false },
//   { id: 4, text: "Practice chaining", done: false },
// ]

Filter students by grade bracket

const students = [
  { name: "Alice", marks: 92 },
  { name: "Bob", marks: 45 },
  { name: "Carol", marks: 78 },
  { name: "Dave", marks: 88 },
  { name: "Eve", marks: 55 },
  { name: "Frank", marks: 96 },
];

function getByGrade(students, grade) {
  const brackets = {
    A: s => s.marks >= 90,
    B: s => s.marks >= 80 && s.marks < 90,
    C: s => s.marks >= 70 && s.marks < 80,
    D: s => s.marks >= 60 && s.marks < 70,
    F: s => s.marks < 60,
  };
  return students.filter(brackets[grade] || (() => false));
}

console.log(getByGrade(students, "A"));
// [{ name: "Alice", marks: 92 }, { name: "Frank", marks: 96 }]

console.log(getByGrade(students, "F"));
// [{ name: "Bob", marks: 45 }, { name: "Eve", marks: 55 }]

Age-based access control

const users = [
  { name: "Alice", age: 25 },
  { name: "Bob", age: 17 },
  { name: "Carol", age: 30 },
  { name: "Dave", age: 15 },
];

const adults = users.filter(u => u.age >= 18);
const minors = users.filter(u => u.age < 18);

11. filter vs manual loop

// Imperative approach
const result = [];
for (const num of nums) {
  if (num > 10) {
    result.push(num);
  }
}

// Declarative approach — same result, less boilerplate
const result2 = nums.filter(n => n > 10);

The filter version is shorter, more readable, and less error-prone (no manual array management).


12. Edge cases

// Empty array — returns empty array
[].filter(n => n > 0); // []

// All pass — returns shallow copy
[1, 2, 3].filter(() => true); // [1, 2, 3] (new array, same elements)

// None pass — returns empty array
[1, 2, 3].filter(() => false); // []

// Sparse arrays — filter skips holes
const sparse = [1, , 3, , 5];
sparse.filter(n => true); // [1, 3, 5] — holes removed!

Key takeaways

  1. filter() returns a new array containing only elements whose callback returned truthy.
  2. It never mutates the original and never returns null — worst case is [].
  3. filter(Boolean) is the idiomatic way to remove falsy values from an array.
  4. Chain filter before map — reduce the data set, then transform.
  5. The callback can test multiple conditions using &&, ||, etc.
  6. For unique values, filter with indexOf works but new Set() is faster.

Explain-It Challenge

Explain without notes:

  1. What does filter return if no elements pass the test?
  2. Why is arr.filter(Boolean) useful — what does it remove?
  3. You need a list of engineer names from an array of employee objects. Would you use filter, map, or both? In what order?

Navigation: ← 1.22.a — map() · 1.22.c — reduce() →