map, filter, reduce, find, findIndex, some, every, flat, flatMap, at, groupBy, toSorted, toSpliced — not the textbook definitions, but the specific moment in a real codebase where each one is the right tool and everything else is the wrong one.
Every JavaScript tutorial explains array methods with fruit arrays. ['apple', 'banana', 'cherry'].filter(fruit => fruit.length > 5). That tells you the syntax. It tells you nothing about when you’re in a codebase at 11pm looking at an array of API responses and you can’t remember whether you want find or filter, some or every, flatMap or flat followed by map. This post is for that moment. Every method paired with the specific, named situation where it belongs — and the adjacent situation where a different method belongs instead.
map — Transform Every Item, Keep the Same Count
The moment it belongs: you have an array of one shape and need an array of a different shape, same length.
// API returns users with snake_case keys
// Your component expects camelCase
const apiUsers = [
{ first_name: 'Sadique', last_name: 'Ali', is_admin: true },
{ first_name: 'Priya', last_name: 'Sharma', is_admin: false },
]
const users = apiUsers.map(user => ({
firstName: user.first_name,
lastName: user.last_name,
isAdmin: user.is_admin,
fullName: `${user.first_name} ${user.last_name}`,
}))
// Every input produces exactly one output
// Result length === input length, always
When it’s the wrong choice: you need a different count in the output. If you’re transforming and filtering at the same time, map then filter works but flatMap is cleaner. If you need one value from the array, not an array of transformed values, you want find or reduce.
The bug that shows you’ve picked the wrong method:
// ❌ Using map when you want filter
const admins = users.map(user => {
if (user.isAdmin) return user
// returns undefined for non-admins — result has undefineds mixed in
})
// ✅ filter removes items, map transforms them
const admins = users.filter(user => user.isAdmin)
filter — Keep Some Items, Remove Others, Same Shape
The moment it belongs: the same array shape in and out, but with fewer items. The predicate returns true for items to keep.
// Shopping cart: show only items that are in stock
const cartItems = [
{ name: 'Widget', inStock: true, price: 29 },
{ name: 'Gadget', inStock: false, price: 49 },
{ name: 'Doohickey', inStock: true, price: 19 },
]
const availableItems = cartItems.filter(item => item.inStock)
// [{ name: 'Widget', ... }, { name: 'Doohickey', ... }]
// Multiple conditions chain naturally
const affordableInStock = cartItems
.filter(item => item.inStock)
.filter(item => item.price < 40)
// Or combined
const affordableInStock = cartItems
.filter(item => item.inStock && item.price < 40)
The adjacent situation: you want find not filter when you expect exactly one result and want the item itself, not an array containing one item.
// ❌ filter when you mean find
const adminUsers = users.filter(user => user.id === targetId)
const admin = adminUsers[0] // tedious, and array might have zero or two results
// ✅ find when you're looking for one specific item
const admin = users.find(user => user.id === targetId)
// Returns the item or undefined — never an array
reduce — Collapse the Entire Array Into One Thing
The moment it belongs: you need a single value computed from the whole array. Not one item from the array — a new value derived from all items.
// Order total with discounts and tax
const orderLines = [
{ quantity: 2, unitPrice: 29.99, discount: 0.1 },
{ quantity: 1, unitPrice: 49.99, discount: 0 },
{ quantity: 3, unitPrice: 9.99, discount: 0.2 },
]
const subtotal = orderLines.reduce((total, line) => {
const lineTotal = line.quantity * line.unitPrice * (1 - line.discount)
return total + lineTotal
}, 0) // 0 is the starting value
// Grouping — reduce to a different shape
const byStatus = orders.reduce((groups, order) => {
const status = order.status
if (!groups[status]) groups[status] = []
groups[status].push(order)
return groups
}, {})
// { pending: [...], shipped: [...], delivered: [...] }
// Counting — reduce to a Map
const statusCounts = orders.reduce((counts, order) => {
counts.set(order.status, (counts.get(order.status) ?? 0) + 1)
return counts
}, new Map())
When it’s the wrong choice: when you’re using reduce to produce an array of the same length as the input. That’s map. When you’re using reduce to produce a subset of the input array. That’s filter. reduce is for when the output type or shape is genuinely different from a 1-to-1 or 1-to-some transformation.
The misuse that appears most often:
// ❌ reduce used as map — obscures intent, no benefit
const prices = products.reduce((acc, product) => {
acc.push(product.price)
return acc
}, [])
// ✅ map is the right tool
const prices = products.map(product => product.price)
find — Get the First Item That Matches
The moment it belongs: you’re looking for one specific item in a collection. You have a unique identifier or a condition that should match at most one item.
// Find the currently selected option in a dropdown
const options = [
{ id: 1, label: 'Small', value: 's' },
{ id: 2, label: 'Medium', value: 'm' },
{ id: 3, label: 'Large', value: 'l' },
]
const selected = options.find(opt => opt.value === currentValue)
// Returns the option object or undefined if not found
// Find a user by ID in local state
const user = this.users.find(u => u.id === userId)
if (!user) {
throw new Error(`User ${userId} not found in local state`)
}
// Find an item in a Pinia store getter
const projectById = (state) => (id) => state.projects.find(p => p.id === id)
The undefined return is the feature, not the bug. It tells you the item doesn’t exist. filter always returns an array — an empty array when nothing matches doesn’t force you to handle the not-found case explicitly. find returning undefined does.
findIndex — Get the Position, Not the Item
The moment it belongs: you need to know where something is in the array, typically because you’re about to replace or remove it.
// Optimistic UI update — update an item in the local array
// before the server confirms the change
function updateProject(updatedProject) {
const index = projects.value.findIndex(p => p.id === updatedProject.id)
if (index === -1) {
console.error('Project not found for optimistic update')
return
}
// Replace the item at that position
projects.value[index] = updatedProject
}
// Remove an item by ID
function removeTask(taskId) {
const index = tasks.value.findIndex(t => t.id === taskId)
if (index !== -1) {
tasks.value.splice(index, 1)
}
}
The -1 return for not-found is the convention. Always check it before using the index for mutation. The alternative — indexOf — only works with primitive values and reference equality. findIndex works with any predicate.
some — Does at Least One Item Match?
The moment it belongs: you need a boolean answer. You don’t need the item. You don’t need all the matching items. You just need to know if any exist.
// Can the user check out?
const cartIsValid = cartItems.some(item => item.quantity > 0 && item.inStock)
// Show an "errors present" indicator on a form
const hasErrors = Object.values(formErrors).some(errors => errors.length > 0)
// Check if any task is overdue before showing an alert
const hasOverdueTasks = tasks.some(task =>
task.dueDate && new Date(task.dueDate) < new Date() && task.status !== 'completed'
)
// Early exit matters — some() stops on the first true
// Don't use filter().length > 0 when you just need a boolean
The comparison that matters:
// ❌ filter to check existence — creates array you don't need
if (users.filter(u => u.isAdmin).length > 0) { /* ... */ }
// ✅ some — short-circuits on first match, no intermediate array
if (users.some(u => u.isAdmin)) { /* ... */ }
some short-circuits: it stops iterating the moment it finds a match. On a 10,000-item array where the first item matches, some does one comparison. filter does 10,000.
every — Do All Items Match?
The moment it belongs: validation. You need to know that the entire collection satisfies a condition before taking an action.
// Form validation — all required fields filled?
const allFieldsFilled = requiredFields.every(field => {
const value = formData[field]
return value !== null && value !== undefined && value !== ''
})
// Can we proceed? All items must be valid
const allItemsValid = cartItems.every(item =>
item.quantity > 0 &&
item.quantity <= item.stockLevel &&
item.inStock
)
// Are all selected files within the size limit?
const allFilesAccepted = selectedFiles.every(file => file.size <= MAX_FILE_SIZE)
// Feature flag — is this feature enabled for all selected tenants?
const featureEnabledForAll = selectedTenants.every(tenant =>
tenant.features.includes('advanced-reporting')
)
every also short-circuits: it stops on the first false. The empty array edge case: [].every(condition) returns true (vacuous truth). Decide whether an empty collection should mean “all valid” or “none to validate” for your specific use case.
flat — Collapse Nested Arrays
The moment it belongs: you have arrays inside arrays and you need to work with a single-level collection.
// API returns paginated results — array of pages, each page is an array
const pages = [
[{ id: 1 }, { id: 2 }, { id: 3 }],
[{ id: 4 }, { id: 5 }, { id: 6 }],
[{ id: 7 }, { id: 8 }],
]
const allItems = pages.flat()
// [{ id: 1 }, { id: 2 }, ..., { id: 8 }]
// Tags on posts — each post has an array of tags
const posts = [
{ title: 'Vue 3', tags: ['vue', 'frontend', 'javascript'] },
{ title: 'Laravel', tags: ['laravel', 'php', 'backend'] },
]
const allTags = posts.map(post => post.tags).flat()
// ['vue', 'frontend', 'javascript', 'laravel', 'php', 'backend']
// But flatMap is cleaner for this — see next section
// Deeply nested — flat(Infinity) collapses all levels
const deepNested = [1, [2, [3, [4]]]].flat(Infinity)
// [1, 2, 3, 4]
The default depth is 1. Pass a number for deeper flattening. flat(Infinity) collapses all nesting levels.
flatMap — Map Then Flatten in One Pass
The moment it belongs: when each item in the array produces zero, one, or multiple items in the result — and you want the result flat.
// Each order has multiple line items — get all line items across all orders
const orders = [
{ id: 1, lines: [{ product: 'Widget', qty: 2 }, { product: 'Gadget', qty: 1 }] },
{ id: 2, lines: [{ product: 'Doohickey', qty: 3 }] },
]
// flatMap is one pass vs map + flat which is two
const allLines = orders.flatMap(order => order.lines)
// [{ product: 'Widget', qty: 2 }, { product: 'Gadget', qty: 1 }, { product: 'Doohickey', qty: 3 }]
// The zero-items case: conditional expansion
// flatMap returning [] effectively filters the item out
const processedItems = items.flatMap(item => {
if (item.status === 'cancelled') return [] // exclude
if (item.type === 'bundle') return item.components // expand
return [item] // keep as-is
})
// Tags again — flatMap is the idiomatic version
const allTags = posts.flatMap(post => post.tags)
// cleaner than posts.map(p => p.tags).flat()
The zero-items return [] is flatMap‘s superpower: it gives you map-and-filter in one pass. An item that returns [] is excluded from the result. An item that returns [a, b] contributes two items. An item that returns [item] is kept as-is. This replaces filter then map in many real scenarios.
at — Access Items From the End Without Math
The moment it belongs: you need the last item, or the second-to-last, without computing array.length - 1.
// Last item in a timeline
const latestActivity = activityLog.at(-1)
// Same as activityLog[activityLog.length - 1] but readable
// Second-to-last
const previousActivity = activityLog.at(-2)
// Works with strings too
const lastChar = filename.at(-1) // file extension character
// In a real use case: most recent error from a log
const mostRecentError = errorLog
.filter(entry => entry.level === 'error')
.at(-1)
// Safe: returns undefined (not an error) if the array is empty
// activityLog.at(-1) === undefined when activityLog is empty
Before at, the last-item pattern was array[array.length - 1]. The negative index syntax matches how Python, Ruby, and most other languages handle reverse indexing. It’s available in all modern browsers and Node 16.6+.
Object.groupBy — Group Items Into Buckets
The moment it belongs: you have a flat array that you need to split into groups based on a shared property. Previously required reduce, now has a dedicated method.
// Group tasks by status for a Kanban board
const tasks = [
{ id: 1, title: 'Design wireframes', status: 'in-progress' },
{ id: 2, title: 'Write tests', status: 'todo' },
{ id: 3, title: 'Deploy to staging', status: 'done' },
{ id: 4, title: 'Code review', status: 'in-progress' },
{ id: 5, title: 'Update docs', status: 'todo' },
]
const byStatus = Object.groupBy(tasks, task => task.status)
// {
// 'in-progress': [{ id: 1, ... }, { id: 4, ... }],
// 'todo': [{ id: 2, ... }, { id: 5, ... }],
// 'done': [{ id: 3, ... }],
// }
// Group orders by month for reporting
const byMonth = Object.groupBy(orders, order => {
const date = new Date(order.createdAt)
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`
})
// Group by computed bucket — e.g. price tier
const byPriceTier = Object.groupBy(products, product => {
if (product.price < 25) return 'budget'
if (product.price < 100) return 'mid-range'
return 'premium'
})
Object.groupBy arrived in Chrome 117, Firefox 119, and Safari 17.4 (late 2023). Node.js 21+. For environments that need earlier support, the reduce version is the polyfill:
// Polyfill for Object.groupBy
const groupBy = (array, keyFn) => array.reduce((groups, item) => {
const key = keyFn(item)
;(groups[key] ??= []).push(item)
return groups
}, {})
toSorted — Sort Without Mutating the Original
The moment it belongs: you want a sorted version of an array without modifying the original. The original sort() mutates in place. toSorted() returns a new array.
// Display products sorted by price without changing the store's order
// In a Pinia store or Vue composable:
const sortedByPrice = computed(() =>
products.value.toSorted((a, b) => a.price - b.price)
)
// products.value is unchanged — sortedByPrice is a new sorted array
// Sort by name for a dropdown
const options = availableProjects.toSorted((a, b) =>
a.name.localeCompare(b.name)
)
// Sort descending by date
const recentFirst = notifications.toSorted((a, b) =>
new Date(b.createdAt) - new Date(a.createdAt)
)
The problem with the original sort:
// ❌ sort mutates — your store state or prop is now sorted
const sorted = products.sort((a, b) => a.price - b.price)
// products is now sorted too. Side effect. Unexpected in a computed.
// ✅ toSorted returns a new array
const sorted = products.toSorted((a, b) => a.price - b.price)
// products unchanged. sorted is a new array.
In Vue computed properties and React useMemo, mutating the source array causes subtle bugs where computed values appear to update the base state. toSorted prevents this class of bug entirely. Available in all modern browsers and Node 20+.
toSpliced — Replace or Remove Items Without Mutating
The moment it belongs: you need to insert, replace, or remove items from an array and return a new array — not modify the existing one. The original splice() mutates.
// Remove an item from a reactive list by index
// Without mutating the original (important in Vue computed properties)
const withoutItem = items.toSpliced(indexToRemove, 1)
// Replace an item at a specific index
const withUpdatedItem = items.toSpliced(indexToReplace, 1, updatedItem)
// Insert an item at a position
const withInserted = items.toSpliced(insertAtIndex, 0, newItem)
// Real use case: optimistic update in a Vue composable
function removeTask(taskId) {
const index = tasks.value.findIndex(t => t.id === taskId)
if (index === -1) return
// Optimistic: update UI immediately
const previousTasks = tasks.value
tasks.value = tasks.value.toSpliced(index, 1)
// Rollback on error
api.deleteTask(taskId).catch(() => {
tasks.value = previousTasks
})
}
The parameters mirror splice: toSpliced(startIndex, deleteCount, ...itemsToInsert).
The Methods Together — When They Chain and When They Don’t
Real code chains array methods. The question is which order and which combination:
// Build a Kanban column: filter to status, sort by priority, take top 10
const todoColumn = tasks
.filter(t => t.status === 'todo')
.toSorted((a, b) => b.priority - a.priority)
.slice(0, 10)
.map(t => ({
id: t.id,
title: t.title,
assignee: t.assignee?.name ?? 'Unassigned',
dueIn: getDaysUntil(t.dueDate),
}))
// Check if a form section is complete
const billingComplete = billingFields.every(field =>
formData[field] !== '' && formData[field] !== null
)
// Total cost of items the user can actually buy
const purchasableTotal = cartItems
.filter(item => item.inStock && item.quantity > 0)
.reduce((total, item) => total + (item.price * item.quantity), 0)
// All unique tags across all selected posts (flat, then deduplicate)
const selectedTags = [...new Set(
selectedPosts.flatMap(post => post.tags)
)]
// Most recent unread notification
const nextToRead = notifications
.filter(n => !n.read)
.at(-1) // last unread — or at(0) for first unread
The rule for deciding which to reach for: start with what the output should be.
Output is a new array, same length → map
Output is a new array, fewer items → filter
Output is a single value or new shape → reduce
Output is a boolean (any match?) → some
Output is a boolean (all match?) → every
Output is one item from the array → find
Output is a position (index) → findIndex
Output is the last item (or nth from end) → at(-n)
Output is a flat array from nested → flat or flatMap
Output is a grouped object → Object.groupBy
Output is a sorted copy (no mutation) → toSorted
Output is a copy with items changed → toSpliced
That table is the answer to “I can’t remember whether I want X or Y.” Describe the output shape. The method follows from the shape.
The Ones That Look Alike But Aren’t
The confusion pairs that show up most often in code review:
// find vs filter
find(fn) → first item matching fn, or undefined
filter(fn) → all items matching fn, as an array
// Use find when you expect at most one result
// Use filter when you expect zero or more results
// some vs every
some(fn) → true if at least one item passes fn
every(fn) → true only if all items pass fn
// some: "is there any?"
// every: "are they all?"
// map vs flatMap
map(fn) → one output per input, same count, one level
flatMap(fn) → zero or more outputs per input, flattened one level
// flatMap returning [] filters; returning [a, b] expands
// flat vs flatMap
flat(n) → flatten n levels deep, no transformation
flatMap(fn) → map then flatten 1 level, with transformation
// flat when you already have arrays and just need them merged
// flatMap when you're producing the nested arrays in the map step
// sort vs toSorted
sort(fn) → sorts in place, returns the same (now sorted) array
toSorted(fn) → returns a new sorted array, original unchanged
// In Vue computed properties or anywhere mutation is wrong: always toSorted
// splice vs toSpliced
splice(i, n, ...items) → mutates in place, returns removed items
toSpliced(i, n, ...items) → returns new array with changes applied
// Same mutation concern as sort/toSorted
