Module: JavaScript
JavaScript·054·6 MIN READ

054: Array Methods II: Transform, Filter, and Find

TOPICS COVERED: Array Methods II: Transform, Filter, and Find

Learning outcomes

By the end of this lesson, you can:

  • choose map(), filter(), find(), or findIndex() from the result they need;
  • write callbacks using the element, index, and source-array parameters;
  • chain methods in a readable data pipeline;
  • explain which methods create arrays and which return one value;
  • explain that copying array methods do not change the source array's slots.

Retrieval warm-up

Before coding, answer from memory:

  1. What is the first valid index of an array?
  2. Which 042 method returns a section without changing the original: slice() or splice()?
  3. What must an arrow function with braces do to produce a value?

Checkpoint: expect 0, slice(), and an explicit return. Try to predict before running code.

Vocabulary

  • Callback: Function passed to a method and called per element. — Source: MDN: Callback function
  • Transform: map-style operation converting each element into a new output value. — Source: MDN: map
  • Predicate: Callback returning truthy/falsy used to test each element. — Source: MDN: Array.prototype.filter()
  • Selection: filter/find/findIndex choosing subset or first match. — Source: MDN: find
  • Chain: Sequencing array methods where each returns a new array. — Source: MDN: map
  • Source array: Original array consumed by a non-mutating pipeline step. — Source: MDN: map
  • Dense array: Array holding a value at every index from 0 through length-1. — Source: MDN: Array
  • Predicate (official): "A predicate is a function that returns true or false to test a condition." — Source: MDN: Array.prototype.filter()
  • Data pipeline (official): "Chaining array methods (map, filter, find) to transform data step by step." — Source: MDN: Array methods — Iterative methods

Beginner mental model

Imagine values moving along a conveyor belt. The callback inspects one value at a time.

  • map() gives every visited value one output ticket. For a dense three-element input, it produces a result of length three. Use it to transform.
  • filter() opens or closes a gate. Only values whose predicate is truthy enter the new array.
  • find() stops at the first matching value and returns that element. It returns undefined when none matches.
  • findIndex() stops at the first match but returns its position. It returns -1 when none matches.

For the dense arrays used in this lesson, map() calls the callback once per element and preserves length. On a sparse array, it skips empty slots and leaves corresponding empty slots in its result, so "every element" does not mean "every numeric index." Prefer dense arrays while learning these methods.

These methods do not add, remove, or reorder slots in the source array. A callback can still cause unrelated side effects, so keep callbacks focused:

js
const prices = [20, 16, 45];
const salePrices = prices.map((price) => price - 2);

console.log(prices);     // [20, 16, 45]
console.log(salePrices); // [18, 14, 43]

Object references and object copying are not required today. They are introduced in later lessons. Every callback can receive (element, index, array). Use only parameters that make the intent clearer. The third parameter is the array currently being processed, not the result being built.

Worked beginner example: build a product shelf

Represent today's shelf as product-name strings. The example needs display labels and two kinds of search.

js
const productNames = ["Notebook", "Desk Lamp", "Water Bottle", "Backpack"];
const affordableNames = ["Notebook", "Water Bottle"];

const shelfLabels = affordableNames.map(
  (name, index) => `${index + 1}. ${name}`,
);
console.log(shelfLabels);

const requestedName = productNames.find((name) => name === "Water Bottle");
if (requestedName === undefined) {
  console.log("Product not found");
} else {
  console.log(requestedName);
}

const lampIndex = productNames.findIndex((name) => name === "Desk Lamp");
console.log(lampIndex);

const longNames = productNames.filter((name) => name.length > 8);
console.log(longNames);

Walk-through:

  1. map() converts each of the two selected names to exactly one label. Its index starts at zero, so the display number uses index + 1.
  2. find() returns the actual first matching string, not an array.
  3. The explicit undefined check handles failed search without syntax from later lessons.
  4. findIndex() returns 1, the position of Desk Lamp. Do not test it with if (lampIndex): index 0 is falsy and -1 is truthy. Compare with -1 explicitly.
  5. filter() can return any number of matching values, including an empty array.

Output:

text
["1. Notebook", "2. Water Bottle"]
Water Bottle
1
["Water Bottle"]

Selection and transformation can become a chain:

js
const shortUppercaseNames = productNames
  .filter((name) => name.length <= 8)
  .map((name) => name.toUpperCase());

console.log(shortUppercaseNames); // ["NOTEBOOK", "BACKPACK"]

Read chains top to bottom: start with names, keep short ones, then transform each kept name. Store an intermediate value when a chain becomes difficult to explain.

Intermediate example: reusable catalog queries

Functions can package a primitive-array search without introducing records:

js
const normalize = (text) => text.trim().toLowerCase();

function searchNames(names, query, minimumLength = 0) {
  const term = normalize(query);

  return names
    .filter((name) => name.length >= minimumLength)
    .filter((name) => normalize(name).includes(term))
    .map((name) => name.toUpperCase());
}

console.log(searchNames(productNames, " bottle ", 8));
// ["WATER BOTTLE"]
console.log(productNames);
// ["Notebook", "Desk Lamp", "Water Bottle", "Backpack"]

The function returns new presentation strings without changing the source array. Multiple filters are acceptable when they make rules easy to name and debug. One combined predicate may traverse fewer times, but clarity matters first for a small list.

Optional advanced extension

Use the callback's third argument to compare a number with its neighbor after filtering:

js
const prices = [4, 28, 16, 45];
const changes = prices
  .filter((price) => price <= 30)
  .map((price, index, affordable) =>
    index === 0 ? 0 : price - affordable[index - 1]
  );

console.log(changes); // [0, 24, -12]

Here affordable is the intermediate filtered array. It is not prices and not changes. This parameter is useful occasionally; capturing a clearly named intermediate array is often simpler.

Object mutation and spread are a preview only, not required today. After objects are taught, this pattern will matter:

js
const previewProducts = [{ name: "Mouse", price: 20 }];
const previewSale = previewProducts.map((product) => ({
  ...product,
  price: 18,
}));

Changing product.price inside the callback would mutate a shared object. The spread copy avoids that, but 046 explains the syntax and shallow-copy rules. Do not assign this preview as a core exercise.

Common mistakes and debugging

  • Missing return: map(name => { name.toUpperCase() }) produces undefined entries. Add return, or remove braces.
  • Using map() to filter: returning conditionally still creates one result slot per input. Use filter().
  • Expecting an array from find(): it returns one element or undefined. Use filter() for all matches.
  • Confusing findIndex() with an ID: an index is an array position and may change when data changes.
  • Testing an index as a boolean: use index !== -1 or index === -1.
  • Side effects in a callback: changing outside state makes a pipeline harder to reason about. Return the transformed value instead.
  • Over-chaining: name intermediate results and log their shape after each stage.
  • Passing a multi-parameter function blindly: map(parseInt) also passes the index as parseInt's radix. Prefer strings.map(text => Number.parseInt(text, 10)).

Best practices

  • Choose by result shape: transform with map, select many with filter, search one with find, locate with findIndex.
  • Name callback parameters after the domain (name or price), not vague letters.
  • Keep predicates free of unrelated side effects.
  • Remember that these methods preserve source slots but cannot prevent callback side effects.
  • Break a chain when an intermediate name explains a business concept.
  • Handle find() failure deliberately before using the returned value.

Checkpoint

Use four result requests without naming methods: "one label per name," "all prices under $20," "the first name containing Bottle," and "the position of Desk Lamp." Identify the method, expected result type, and failure value before writing code. Then change the order and determine which answers remain stable. Finally, point at every callback return and describe what that return means to its particular method.

Exercises

Core

From productNames, create uppercaseNames. Then find "Backpack" and print the found string or "Missing" using an explicit undefined check.

js
const uppercaseNames = productNames.map((name) => name.toUpperCase());
const foundName = productNames.find((name) => name === "Backpack");

console.log(uppercaseNames);
if (foundName === undefined) {
  console.log("Missing");
} else {
  console.log(foundName);
}

Output:

text
["NOTEBOOK", "DESK LAMP", "WATER BOTTLE", "BACKPACK"]
Backpack

Practice

Write getPassingLabels(scores, minimum) that keeps scores at least minimum, then returns labels like "Passing score: 82". Confirm the source is unchanged.

js
function getPassingLabels(scores, minimum) {
  return scores
    .filter((score) => score >= minimum)
    .map((score) => `Passing score: ${score}`);
}

const scores = [45, 82, 67, 91];
console.log(getPassingLabels(scores, 70));
console.log(scores);

Output:

text
["Passing score: 82", "Passing score: 91"]
[45, 82, 67, 91]

Professional Extension

Write replaceFirst(values, target, replacement). Return a new array in which only the first matching primitive value is replaced. If the target is missing, return an unchanged array copy.

js
function replaceFirst(values, target, replacement) {
  const targetIndex = values.findIndex((value) => value === target);
  return values.map((value, index) =>
    index === targetIndex ? replacement : value
  );
}

const names = ["plan", "code", "test", "code"];
const updated = replaceFirst(names, "code", "build");
console.log(updated);
console.log(names);
console.log(replaceFirst(names, "deploy", "ship") === names);

Output:

text
["plan", "build", "test", "code"]
["plan", "code", "test", "code"]
false

Even when the target is absent, map() returns a different array reference. Its primitive values are safe to reuse directly.

Recap

Explain without notes: For a dense array, which method returns an array of the same length? What happens to sparse holes? Which method returns zero or more matches? What are the failure values of find() and findIndex()? Why should callbacks avoid unrelated side effects?

Official references