FullStack Course LogoFullStack Course
Module: JavaScript
JavaScript·054·8 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 should be able to:

  • choose map(), filter(), find(), or findIndex() based on the result you need;
  • write callbacks that use the element, index, and source-array parameters when appropriate;
  • chain methods into a readable data pipeline;
  • explain which methods create arrays and which return a single value;
  • explain that copying array methods do not change the source array's slots.

Retrieval warm-up

Before you write any code, answer these 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 when it needs to produce a value?

Checkpoint: the expected answers are 0, slice(), and an explicit return. Make a prediction before you run anything; that small habit makes the result more useful when you are debugging.

Vocabulary

  • Callback: A function passed to a method that the method calls once per element. — Source: MDN: Callback function
  • Transform: A map-style operation that converts each element into a new output value. — Source: MDN: map
  • Predicate: A callback whose truthy or falsy result tests each element. — Source: MDN: Array.prototype.filter()
  • Selection: The act of choosing a subset, or the first matching value, with filter(), find(), or findIndex(). — Source: MDN: find
  • Chain: A sequence of array methods in which one method's result becomes the next method's input. When the intermediate results are arrays, this forms a data pipeline. — Source: MDN: map
  • Source array: The original array consumed by a non-mutating pipeline step. — Source: MDN: map
  • Dense array: An array with 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

Picture the values moving along a conveyor belt. The callback looks at one value at a time, and the method decides what happens to that value based on the callback's result.

  • map() gives every visited value exactly one output ticket. For a dense three-element input, the result has length three. Use map() when you are transforming values.
  • filter() acts like a gate. A value enters the new array only when its predicate result is truthy.
  • find() stops as soon as it reaches the first matching value and returns that element. If nothing matches, it returns undefined.
  • findIndex() also stops at the first match, but returns that element's position. If nothing matches, it returns -1.

The conveyor-belt picture is useful for understanding one callback invocation at a time, but it is not a complete model of JavaScript execution. In particular, these methods do not mutate the source array's slots. Also, a callback can still create unrelated side effects, so keep callbacks focused on producing the value or decision the method needs.

For the dense arrays used in this lesson, map() calls the callback once for each element and preserves the array's length. Sparse arrays are the important exception: map() skips empty slots and leaves corresponding empty slots in its result. Therefore, "every element" does not always mean "every numeric index." Dense arrays are the better starting point while you learn these methods.

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 outside today's required material; later lessons introduce them. Every callback can receive (element, index, array). Use only the parameters that make the intent clearer. The third parameter is the array currently being processed, not the result that the method is building.

Worked beginner example: build a product shelf

For this first example, keep the data simple: today's shelf is represented as product-name strings. We need display labels for a selected list, a search that returns one product, and a search that returns a position.

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 into exactly one label. The callback's index starts at zero, so the human-facing number is index + 1.
  2. find() returns the first matching string itself, not an array containing that string.
  3. The explicit undefined check handles a failed search without relying on syntax from later lessons.
  4. findIndex() returns 1, the position of Desk Lamp. Do not write if (lampIndex) to test whether it found something: index 0 is falsy, while -1 is truthy. Compare with -1 explicitly instead.
  5. filter() may return any number of matching values, including no values at all, in which case the result is an empty array.

Output:

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

Because filter() and map() both return arrays, they can be composed into a pipeline:

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

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

Read the chain from top to bottom. Start with the names, keep the short ones, and then transform each remaining name to uppercase. If the chain becomes difficult to explain or debug, give an intermediate result a name instead of adding more methods.

Intermediate example: reusable catalog queries

Once the individual operations are clear, a function can package a reusable search over a primitive array without introducing records or object data:

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 produces new presentation strings while leaving the source array unchanged. Multiple filters are perfectly reasonable when each rule is easy to name and inspect. Combining the rules into one predicate could traverse the list fewer times, but for a small list, clarity is the better first priority. Optimize that trade-off only when measurement or a real constraint calls for it.

Optional advanced extension

The callback's third argument can be useful when a later transformation needs to compare an element with its neighbor in an earlier pipeline stage. Here, the comparison happens against the filtered array:

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 neither prices nor changes. The third callback parameter is occasionally useful, but capturing a clearly named intermediate array is often easier for another developer to read and easier to inspect during debugging.

Object mutation and spread are preview material only, not a requirement for today's exercises. After objects have been taught, this pattern becomes relevant:

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 mutation, but 046 explains the syntax and shallow-copy rules in detail. Do not treat this preview as a core exercise.

Common mistakes and debugging

  • Missing return: map(name => { name.toUpperCase() }) produces undefined entries because braces create a function body and do not provide an implicit return. Add return, or remove the braces.
  • Using map() to filter: A conditional return still creates one result slot per input. Use filter() when the result should contain only selected values.
  • Expecting an array from find(): find() returns one element or undefined. Use filter() when you need every match.
  • Confusing findIndex() with an ID: An index is an array position, not a stable identity. It can change when the data is reordered or items are inserted.
  • Testing an index as a boolean: Use index !== -1 to handle success or index === -1 to handle failure.
  • Side effects in a callback: Mutating outside state makes a pipeline harder to reason about. Return the transformed value instead, and keep unrelated state changes out of the callback.
  • Over-chaining: Name intermediate results and inspect their shape after each stage when the pipeline is not obvious.
  • Passing a multi-parameter function blindly: map(parseInt) also supplies the index as parseInt's radix. Prefer strings.map(text => Number.parseInt(text, 10)) so the radix is explicit.

Best practices

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

Checkpoint

Consider these four result requests without naming the methods: "one label per name," "all prices under $20," "the first name containing Bottle," and "the position of Desk Lamp." For each one, identify the method, the expected result type, and the failure value before writing code. Then change the order of the data and determine which answers remain stable. Finally, point to every callback return and explain what that return means for the particular method receiving it.

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) so it keeps scores at least minimum, then returns labels such as "Passing score: 82". Confirm that the source remains 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). It should return a new array in which only the first matching primitive value is replaced. If the target is absent, return an unchanged array copy rather than the original array.

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() creates a different array reference. Since these values are primitives, reusing the unchanged values directly is safe.

Recap

Without looking at your notes, explain the following: 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

Reader page: /javascript/lesson/054/array-methods-ii-transform-filter-and-find