Step 1 of 5 · Reading · ~4 min
Learn
Collections
Pipelines: Map, Filter, Reduce
Loops tell the computer HOW to iterate. filter, map and reduce say WHAT you want: keep these, transform them, collapse them to one answer. By the end of this lesson you can build a chain of the three, predict the type that comes out of each stage, and avoid the traps that make a pipeline return NaN, undefined or crash on an empty array.
The three stages
const fahrenheit: number[] = [14, 32, 50, 86];
fahrenheit.filter(f => f > 32); // [50, 86] keep where the callback is true
fahrenheit.map(f => (f - 32) * 5 / 9); // [-10, 0, 10, 30] transform every element
fahrenheit.reduce((acc, f) => acc + f, 0); // 182 fold into one value
filterreturns a NEW array of the elements for which the callback returned true. The element type is unchanged and the array may be shorter. The original is untouched.mapreturns a NEW array of whatever the callback returned: always the same length, possibly a different element type (nums.map(String)isstring[]).reducethreads an accumulator through the array. The callback is(accumulator, element) => newAccumulator, and the second argument toreduceis the accumulator's starting value. The result is a single value, which need not have the element's type.
Chaining
const warmTotal: number = fahrenheit
.filter(f => f > 32) // number[]: [50, 86]
.map(f => (f - 32) * 5 / 9) // number[]: [10, 30]
.reduce((acc, c) => acc + c, 0); // number: 40
Each stage hands its result to the next, and TypeScript infers the type that flows out of every stage. If a map callback accidentally returned a string, the reduce below it would be flagged immediately.
Stages can also change the element type. This turns records into names, then into one string:
type Pet = { name: string; age: number };
const pets: Pet[] = [{ name: "Rex", age: 3 }, { name: "Mia", age: 9 }, { name: "Bo", age: 1 }];
const old: string = pets.filter(p => p.age > 2).map(p => p.name).join(" & "); // "Rex & Mia"
Order matters. After map(p => p.name) the elements are strings, so a later filter(p => p.age > 2) would have nothing to read. Filter first, while the fields still exist.
Trap 1: reduce without an initial value
[].reduce((a, b) => a + b); // TypeError: Reduce of empty array with no initial value
[].reduce((a, b) => a + b, 0); // 0
With no initial value, reduce seeds the accumulator from the first element, and an EMPTY array has none, so it throws. Any pipeline whose filter might reject everything needs an explicit initial value.
Trap 2: forgetting to return
A concise arrow body returns automatically. A body in braces needs an explicit return:
nums.map(n => n * n); // fine: implicit return
nums.map(n => { n * n; }); // BUG: returns undefined for every element
nums.map(n => { return n * n; }); // fine
The buggy version yields [undefined, undefined, ...], and later arithmetic on those turns into NaN.
Trap 3: passing a function with extra parameters
map calls its callback with (element, index, array). A function that takes a second parameter gets the index:
["10", "10", "10"].map(parseInt); // [10, NaN, 2]: parseInt(x, index) reads the index as a radix
["10", "10", "10"].map(s => parseInt(s)); // [10, 10, 10]
["10", "10", "10"].map(Number); // [10, 10, 10]: Number ignores extra arguments
Common mistakes
- No initial value on
reduce. It crashes the moment the filter rejects everything. - Braces without
return. Every element becomesundefined. - Transforming before filtering. The fields you wanted to test are already gone.
>where the rule says "at least". The boundary value is silently dropped.
Your exercise
Summarise an order. The starter parses each input line into an object. You then use a pipeline over those objects to find the lines that qualify, list their names, and add up what they cost. The hidden tests include an order where nothing qualifies and a line sitting exactly on the boundary.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…