JavaScript and TypeScript have been steadily filling gaps that once forced us to reach for Lodash or Underscore. With new array and object methods, we now have native alternatives that handle non-mutating transformations, grouped queries, and async iteration without adding dependency weight. In this post, I'll walk through the recent additions that are changing how we write everyday data manipulation code.
Why These Methods Matter
For years, immutability was something we had to think about carefully. Sorting an array meant creating a copy, filtering meant wrapping logic, and grouping required loops or utility functions. The modern JavaScript spec has recognized this pattern and given us methods that respect immutability by default while offering cleaner APIs.
I've shipped production systems where the team maintained a small utils folder just for these operations. Today, that folder can shrink significantly. When you're working on e-commerce platforms or CRM dashboards where state management matters, having native immutable methods is a win for both readability and preventing bugs.
toSorted, toSpliced, and with — Non-Mutating Transformations
The oldest pain point: Array.prototype.sort() mutates the original array. This catches people off guard constantly. Even experienced developers forget and cause subtle bugs where the original data changes unexpectedly.
toSorted() is the immutable version. It returns a new sorted array without touching the original.
const scores = [42, 18, 95, 7, 61];
// Old approach: spread and sort
const sorted1 = [...scores].sort((a, b) => a - b);
console.log(scores); // [42, 18, 95, 7, 61] — still untouched
// New approach: toSorted
const sorted2 = scores.toSorted((a, b) => a - b);
console.log(scores); // [42, 18, 95, 7, 61] — definitely still untouched
console.log(sorted2); // [7, 18, 42, 61, 95]In a React component managing a leaderboard, this is safer. You're guaranteed the original state array stays intact even if you forget to spread it.
toSpliced() works the same way for splice(). The method returns a new array with elements removed or replaced, leaving the original alone.
const items = ['apple', 'banana', 'cherry', 'date'];
// Old: splice mutates
items.splice(1, 2, 'blueberry', 'blackberry');
console.log(items); // mutated
// New: toSpliced returns new array
const updated = items.toSpliced(1, 2, 'blueberry', 'blackberry');
console.log(items); // ['apple', 'banana', 'cherry', 'date'] unchanged
console.log(updated); // ['apple', 'blueberry', 'blackberry', 'date']The with() method is for simple index replacement. Instead of arr[i] = value, you use arr.with(i, value).
const permissions = ['read', 'write', 'delete'];
// Immutable update at index 2
const updated = permissions.with(2, 'admin');
console.log(permissions); // ['read', 'write', 'delete']
console.log(updated); // ['read', 'write', 'admin']I use this in permission management features where you're building audit trails. Each operation leaves the original intact, making it easier to track changes.
findLast and findLastIndex
These are simpler additions but solve a real problem: finding elements from the end of an array without reversing or slicing.
findLast() returns the last element that matches a predicate. findLastIndex() returns its index.
const events = [
{ id: 1, type: 'click', timestamp: 100 },
{ id: 2, type: 'scroll', timestamp: 200 },
{ id: 3, type: 'click', timestamp: 300 },
{ id: 4, type: 'resize', timestamp: 400 }
];
// Find the last click event
const lastClick = events.findLast(e => e.type === 'click');
console.log(lastClick); // { id: 3, type: 'click', timestamp: 300 }
// Get its index
const lastClickIndex = events.findLastIndex(e => e.type === 'click');
console.log(lastClickIndex); // 2Why not just reverse and use find()? Performance. On large arrays, findLast() stops searching the moment it finds a match, working backward. It doesn't create a reversed copy.
I've used this in analytics systems where we need the most recent event of a certain type for a user session. Cleaner than the old reverse().find(...) pattern.
Object.groupBy and Map.groupBy
Grouping data is one of the most common operations. Before this, you'd reach for Lodash's _.groupBy() or write a reduce loop. Now we have native methods.
Object.groupBy() returns an object where keys are the group identifiers and values are arrays of items.
const leads = [
{ id: 1, name: 'Alice', status: 'qualified' },
{ id: 2, name: 'Bob', status: 'prospect' },
{ id: 3, name: 'Carol', status: 'qualified' },
{ id: 4, name: 'Dave', status: 'prospect' }
];
const byStatus = Object.groupBy(leads, l => l.status);
console.log(byStatus);
/* Output:
{
qualified: [
{ id: 1, name: 'Alice', status: 'qualified' },
{ id: 3, name: 'Carol', status: 'qualified' }
],
prospect: [
{ id: 2, name: 'Bob', status: 'prospect' },
{ id: 4, name: 'Dave', status: 'prospect' }
]
}
*/This is invaluable in CRM dashboards. You group leads by status or stage, and suddenly you have a clean structure for rendering different sections or building reports.
Map.groupBy() is similar but returns a Map instead. Maps are better when your grouping keys are not simple strings or when you need to use objects as keys.
const transactions = [
{ amount: 100, category: 'food' },
{ amount: 50, category: 'transport' },
{ amount: 200, category: 'food' },
{ amount: 75, category: 'entertainment' }
];
const byCategory = Map.groupBy(transactions, t => t.category);
console.log(byCategory.get('food')); // [{ amount: 100 }, { amount: 200 }]
console.log(byCategory.size); // 3Maps are also useful when you want to iterate and transform grouped data efficiently. They're faster for large datasets and feel more idiomatic when categories are dynamic or numerous.
Array.fromAsync for Async Iterables
When you're working with streams, async generators, or paginated APIs, you often need to collect async iterables into an array. Array.fromAsync() does this while waiting for each item.
// Example: streaming data from an async generator
async function* fetchUserPages(userId: string) {
for (let page = 1; page <= 3; page++) {
const response = await fetch(`/api/users/${userId}/posts?page=${page}`);
const posts = await response.json();
yield* posts;
}
}
// Collect all posts into a single array
const allPosts = await Array.fromAsync(fetchUserPages('user-123'));
console.log(allPosts.length); // total posts across all pagesI use this in ride-hailing dashboards where we're paginating through ride history or in travel platforms pulling routes from multiple sources. Instead of manually looping and pushing, Array.fromAsync() handles the async waiting transparently.
You can also apply a mapping function as the second argument, transforming each item as it arrives.
const normalized = await Array.fromAsync(
fetchUserPages('user-123'),
post => ({ id: post.id, title: post.title.toUpperCase() })
);
Common Mistakes
- Assuming browser support is universal:
toSorted(),toSpliced(), andwith()are relatively new. Check caniuse.com or use TypeScript'slibconfiguration to enforce targeting. For production apps supporting older browsers, polyfills or transpilation might be needed. - Mixing
Object.groupBy()with numeric string keys: JavaScript object keys are always strings. If you group by something that coerces to a number (likeuserIdorstatusCode), be careful with key access. UseMap.groupBy()if your keys are complex objects or need type safety. - Forgetting the async in
Array.fromAsync(): This method is async, so you must await it. Forgettingawaitgives you a Promise, not an array, and bugs cascade from there. - Using
findLast()when you don't need backwards search: It's great for semantic clarity, but if you're just filtering and taking the first match, stick withfind(). Don't optimize prematurely.
Key Takeaways
- Use
toSorted(),toSpliced(), andwith()to avoid mutating arrays in modern codebases. They're cleaner than spread patterns and signal immutability intent clearly. findLast()andfindLastIndex()offer performance benefits on large arrays and read better than reversing and finding.Object.groupBy()andMap.groupBy()replace manual grouping logic. Choose Object for simple string keys, Map for complex or object keys.Array.fromAsync()eliminates boilerplate when collecting async iterables. Pair it with mapping for on-the-fly transformations.- Browser compatibility matters in production. Verify what your target environments support before committing to these APIs in shipped code.






Comments
Loading comments…