Lesson 0.4 · JavaScript in general
Arrays I: map, filter and friends
Indexes, map, filter, find, some, every, forEach and method chaining.
Loading lesson…
Lesson 0.4 · JavaScript in general
Indexes, map, filter, find, some, every, forEach and method chaining.
Loading lesson…
An array is an ordered row of values – like the carriages of a train or crates on a conveyor belt. Each one has its position number (index), and counting starts at zero.
Picture a fruit sorting plant with a conveyor belt. You put a crate of apples on the belt, and depending on which machine stands at the end, you get something different:
map) – every apple goes through the machine and comes out peeled. As many apples come out as went in.filter) – lets only some apples through. The apples don't change, there are just fewer of them.find) – stops the belt at the first apple you're looking for and hands it to you.some/every) – only says "yes" or "no": is there at least one rotten one? Are they all ripe?All these machines work the same way: they get a function (a callback) and call it for every item. You only write what should happen to one item. In React this is your daily bread – every list on a page is made with map, every search with filter.
const fruits = ['apple', 'pear', 'plum']const empty = []const mixed = [1, 'text', true, { a: 1 }, [1, 2]] // an array can hold anythingfruits.length // 3fruits[0] // 'apple' – the first item has index 0fruits[2] // 'plum' – the last one has index length - 1fruits[3] // undefined – out of range isn't an errorfruits.at(-1) // 'plum' – a negative index counts from the endfruits.at(-2) // 'pear'fruits.length === 0 // is the array empty?Array.isArray(fruits) // true – that's how to recognise an array (typeof returns 'object')
The most common shape of data in a real app – and in React too:
const users = [{ id: 1, name: 'Anna', city: 'London' },{ id: 2, name: 'Peter', city: 'Boston' },]users[1].name // 'Peter'users[0].city // 'London'users.at(-1).id // 2
Array.from({ length: 3 }, (_, i) => i + 1) // [1, 2, 3] – an array of a given lengthArray.from('hey') // ['h', 'e', 'y']'a,b,c'.split(',') // ['a', 'b', 'c'][...'hey'] // ['h', 'e', 'y']
const fruits = ['apple', 'pear', 'plum']fruits.includes('pear') // truefruits.includes('banana') // falsefruits.indexOf('plum') // 2fruits.indexOf('banana') // -1 – "not found"
const fruits = ['apple', 'pear', 'plum']// The classic for – when you need the index or want to skipfor (let i = 0; i < fruits.length; i++) {console.log(i, fruits[i])}// for…of – the most readable when you just want to go through the itemsfor (const fruit of fruits) {console.log(fruit)}// for…of with the indexfor (const [i, fruit] of fruits.entries()) {console.log(i, fruit)}// forEach – calls a function for every itemfruits.forEach((fruit, i) => console.log(i, fruit))
| for / for…of | forEach | map | |
|---|---|---|---|
| Returns | nothing (it’s a statement) | undefined | a new array |
| Can you stop it (break)? | yes | no | no |
| When | a loop with break, await inside | a side effect for every item (log, save) | when you want to turn an array into another array |
map calls your function for every item and builds a new array of the same length from its return values. The original array doesn't change.
const numbers = [1, 2, 3, 4]numbers.map((n) => n * 2) // [2, 4, 6, 8]numbers.map((n) => n * n) // [1, 4, 9, 16]numbers.map((n) => `#${n}`) // ['#1', '#2', '#3', '#4']numbers.map((n) => n > 2) // [false, false, true, true]numbers.map(String) // ['1', '2', '3', '4'] – a ready-made function as the callbacknumbers // [1, 2, 3, 4] – unchanged
const users = [{ id: 1, first: 'Anna', last: 'Smith', age: 31 },{ id: 2, first: 'Peter', last: 'Brown', age: 17 },]// Pull out one propertyusers.map((u) => u.id) // [1, 2]// Compute something newusers.map((u) => `${u.first} ${u.last}`) // ['Anna Smith', 'Peter Brown']// Destructuring right in the parameterusers.map(({ first, age }) => `${first} (${age})`) // ['Anna (31)', 'Peter (17)']// A new object with selected properties – MIND the parenthesesusers.map((u) => ({ id: u.id, name: u.first })) // [{ id: 1, name: 'Anna' }, …]// A copy with an added property (the original objects stay as they are)users.map((u) => ({ ...u, isAdult: u.age >= 18 }))
const fruits = ['apple', 'pear', 'plum']fruits.map((fruit, i) => `${i + 1}. ${fruit}`) // ['1. apple', '2. pear', '3. plum']fruits.map((_, i) => i) // [0, 1, 2] – _ = "I don't need this parameter"
filter calls a function for every item. When the function returns true (or anything truthy), the item gets into the new array – whole and unchanged.
const numbers = [5, 12, 8, 130, 44]numbers.filter((n) => n > 10) // [12, 130, 44]numbers.filter((n) => n % 2 === 0) // [12, 8, 130, 44] – even onesnumbers.filter((n) => n > 1000) // [] – no match = an empty array, not undefined
const products = [{ id: 1, name: 'Apple', price: 12, inStock: true, category: 'fruit' },{ id: 2, name: 'Pear', price: 15, inStock: false, category: 'fruit' },{ id: 3, name: 'Carrot', price: 6, inStock: true, category: 'vegetables' },]products.filter((p) => p.inStock) // Apple, Carrotproducts.filter((p) => !p.inStock) // Pearproducts.filter((p) => p.category === 'fruit') // Apple, Pearproducts.filter((p) => p.price >= 10 && p.inStock) // Apple// Remove an item by id – "keep everything EXCEPT"products.filter((p) => p.id !== 2) // Apple, Carrot// Searchingconst query = 'ap'products.filter((p) => p.name.toLowerCase().includes(query.toLowerCase())) // Apple// Throw out empty values['a', '', null, 'b', undefined].filter(Boolean) // ['a', 'b']
const users = [{ id: 1, name: 'Anna', city: 'London' },{ id: 2, name: 'Peter', city: 'Boston' },{ id: 3, name: 'Eve', city: 'Boston' },]users.find((u) => u.id === 2) // { id: 2, name: 'Peter', city: 'Boston' } – an object, not an arrayusers.find((u) => u.city === 'Boston') // Peter – only the FIRST matchusers.find((u) => u.id === 99) // undefinedusers.findIndex((u) => u.id === 3) // 2 – the positionusers.findIndex((u) => u.id === 99) // -1users.findLast((u) => u.city === 'Boston') // Eve – searches from the end
| find | filter | |
|---|---|---|
| Returns | one item (or undefined) | always an array (even an empty one) |
| Stops | at the first match | goes through everything |
| Typically | a detail by id | a list by a condition |
const products = [{ name: 'Apple', price: 12, inStock: true },{ name: 'Pear', price: 15, inStock: false },]products.some((p) => !p.inStock) // true – is at least one sold out?products.some((p) => p.price > 100) // falseproducts.every((p) => p.price < 20) // true – are they all cheaper than 20?products.every((p) => p.inStock) // false// Typical useconst canCheckout = cart.every((item) => item.inStock)const hasErrors = fields.some((f) => f.error)
Because both map and filter return an array, you can call another method on the result straight away. Read it top to bottom like the steps of a recipe:
const result = products.filter((p) => p.inStock) // 1. only in stock.filter((p) => p.price < 20) // 2. only cheap ones.map((p) => p.name.toUpperCase()) // 3. only the names in capitals// The same with intermediate steps – sometimes easier to readconst inStock = products.filter((p) => p.inStock)const cheap = inStock.filter((p) => p.price < 20)const names = cheap.map((p) => p.name.toUpperCase())
What you see: Six products (two sold out), a row of buttons with methods and, below them, the code, an explanation and the result. The highlighted products are the ones the method "took". reduce and toSorted are a taste of the next lesson.
Try it:
The takeaway: Before you reach for a method, ask yourself: do I want back an array of the same length (map), a shorter array (filter), one item (find), or yes/no (some/every)?
Loading the interactive part…
| I want… | Method | Returns |
|---|---|---|
| to turn every item into something else | map | a new array of the same length |
| only some items | filter | a new array (shorter or the same) |
| the first item that meets a condition | find | the item, or undefined |
| the position of the first such item | findIndex | a number, or -1 |
| to know whether at least one meets it | some | true / false |
| to know whether all of them meet it | every | true / false |
| to know whether the array contains a value | includes | true / false |
| to do something for every item (no result) | forEach | undefined |
One call of map and one call of filter – nothing more.
CHALLENGE: Doubles and positive numbers 1. doubleAll([1, 2, 3]) should return [2, 4, 6]. Use map. 2. onlyPositive([-2, 5, 0, 3]) should return [5, 3]. Use filter (positive = greater than 0).
Loading the interactive part…
Eight queries over a task list. Each can be written as a single expression – choose the right method (or a chain of them). The tests run on their own and show which functions already work.
CHALLENGE: Queries over a task list (map, filter, find, some, every, chaining) Write the bodies of eight functions – each as a single expression with an array method. The tests below run on their own and show ✅/❌. You'll find the task data in the Tests section (the `tasks` array). 1. titles(tasks) – an array of task titles (map). 2. openTasks(tasks) – only unfinished tasks, done === false (filter). 3. findById(tasks, id) – the task with the given id, or undefined (find). 4. hasUrgent(tasks) – is there at least one task with priority 'high'? (some) 5. allAssigned(tasks) – do all tasks have an assignee filled in (not null)? (every) 6. search(tasks, query) – tasks whose title contains the search text, ignoring letter case and spaces at the ends of the query (filter + toLowerCase + trim + includes). 7. openTitlesFor(tasks, person) – the titles of a person's UNfinished tasks (filter → map). 8. numbered(tasks) – ['1. Go shopping', '2. Clean the kitchen', …] (map with the second parameter, index).
Loading the interactive part…
arr.at(-1), out of range is undefined.includes/indexOf only for simple values; look for objects with some/find by a property.return and ({ … }).!==.undefined – guard it with ?..true.Checks, deepFreeze, equal, Check)Checks, deepFreeze, equal, Check – from the file src/course/checks.tsx. The course helper for function-style challenges: runs the tests and shows ✅/❌. You don't need to know how it works inside – it just tells you which steps are done./** One test: a description and a function returning true (passed) or false. It may be async. */export type Check = [name: string, test: () => boolean | Promise<boolean>]/*** Runs the tests of a challenge and shows ✅ or ❌ for each.* An error thrown in a test (e.g. when trying to change a frozen object) is shown under the test.*/export function Checks({ tests }: { tests: Check[] }) {const t = useT()const [results, setResults] = useState<Result[]>(() => tests.map(() => ({ status: 'pending' })))useEffect(() => {let ignore = falsetests.forEach(([, test], i) => {Promise.resolve().then(test).then((ok): Result => (ok ? { status: 'ok' } : { status: 'fail' }),(e): Result => ({ status: 'fail', error: e instanceof Error ? e.message : String(e) }),).then((result) => {if (ignore) returnsetResults((prev) => prev.map((r, idx) => (idx === i ? result : r)))})})return () => {ignore = true}}, [tests])const passed = results.filter((r) => r.status === 'ok').lengthreturn (<div className="stack"><strong className={passed === tests.length ? 'ok' : undefined}>{t.checksPassed(passed, tests.length)}</strong><ul className="list">{tests.map(([name], i) => {const r = results[i] ?? { status: 'pending' }return (<li key={name}>{r.status === 'pending' ? '⏳' : r.status === 'ok' ? '✅' : '❌'} {name}{r.status === 'fail' && r.error && (<div className="bad muted">{t.checksError} {r.error}</div>)}</li>)})}</ul></div>)}/** Freezes an object and all nested objects and arrays – any attempt to change them throws. */export function deepFreeze<T>(value: T): T {if (value && typeof value === 'object') {Object.values(value).forEach(deepFreeze)Object.freeze(value)}return value}/** Compares two values by content (arrays and objects item by item), not by reference. */export function equal(a: unknown, b: unknown): boolean {return JSON.stringify(a) === JSON.stringify(b)}
Checks, deepFreeze, equal, Check)Checks, deepFreeze, equal, Check – from the file src/course/checks.tsx. The course helper for function-style challenges: runs the tests and shows ✅/❌. You don't need to know how it works inside – it just tells you which steps are done./** One test: a description and a function returning true (passed) or false. It may be async. */export type Check = [name: string, test: () => boolean | Promise<boolean>]/*** Runs the tests of a challenge and shows ✅ or ❌ for each.* An error thrown in a test (e.g. when trying to change a frozen object) is shown under the test.*/export function Checks({ tests }: { tests: Check[] }) {const t = useT()const [results, setResults] = useState<Result[]>(() => tests.map(() => ({ status: 'pending' })))useEffect(() => {let ignore = falsetests.forEach(([, test], i) => {Promise.resolve().then(test).then((ok): Result => (ok ? { status: 'ok' } : { status: 'fail' }),(e): Result => ({ status: 'fail', error: e instanceof Error ? e.message : String(e) }),).then((result) => {if (ignore) returnsetResults((prev) => prev.map((r, idx) => (idx === i ? result : r)))})})return () => {ignore = true}}, [tests])const passed = results.filter((r) => r.status === 'ok').lengthreturn (<div className="stack"><strong className={passed === tests.length ? 'ok' : undefined}>{t.checksPassed(passed, tests.length)}</strong><ul className="list">{tests.map(([name], i) => {const r = results[i] ?? { status: 'pending' }return (<li key={name}>{r.status === 'pending' ? '⏳' : r.status === 'ok' ? '✅' : '❌'} {name}{r.status === 'fail' && r.error && (<div className="bad muted">{t.checksError} {r.error}</div>)}</li>)})}</ul></div>)}/** Freezes an object and all nested objects and arrays – any attempt to change them throws. */export function deepFreeze<T>(value: T): T {if (value && typeof value === 'object') {Object.values(value).forEach(deepFreeze)Object.freeze(value)}return value}/** Compares two values by content (arrays and objects item by item), not by reference. */export function equal(a: unknown, b: unknown): boolean {return JSON.stringify(a) === JSON.stringify(b)}