Skip to content
ExponaBLOG
JAVASCRIPT & TYPESCRIPT · DAY 017 / 100

New array and object methods in JavaScript & TypeScript

Recent additions that replace utility libraries.

Expona Admin7 min read

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.

typescript
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.

typescript
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).

typescript
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.

typescript
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); // 2

Why 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.

typescript
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.

typescript
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); // 3

Maps 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.

typescript
// 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 pages

I 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.

typescript
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(), and with() are relatively new. Check caniuse.com or use TypeScript's lib configuration 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 (like userId or statusCode), be careful with key access. Use Map.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. Forgetting await gives 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 with find(). Don't optimize prematurely.

Key Takeaways

  • Use toSorted(), toSpliced(), and with() to avoid mutating arrays in modern codebases. They're cleaner than spread patterns and signal immutability intent clearly.
  • findLast() and findLastIndex() offer performance benefits on large arrays and read better than reversing and finding.
  • Object.groupBy() and Map.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.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles