Why flatMap() is easier than filter() in TypeScript apps
Irena
July 14, 2024
Before we dive into the programming bubble, here's a little digression for the curious minds: Wondering about the cat in the picture? Meet the adorable Bärbel! 🐈 Puzzled by the lime green background? It's a nod to internet culture! The more you know!
Take a moment to reflect: What's your favorite Array method?
As a TypeScript/React developer, I genuinely love a good old map function. There's something oddly satisfying about using .map() to iterate over items and create JSX elements. However, for some time now, I've been heavily using flatMap() in my TypeScript code. Let's explore why that's the case and how new additions in TypeScript 5.5 might change that again!
For those who answered my initial question with 'reduce': come on!
Imagine you have an array of elements. You want to transform each element while also filtering out some of them based on a certain condition. Traditionally, you might chain .map() and .filter() together. I used to do this until my colleague Johannes pointed out that I could use flatMap. Intrigued, I delved deeper, and now I want to share what I've learned. Let's look at two of my favorites: TypeScript and Cats 🐈⬛.
const cats: Array<{
chonkFactor: number;
name?: string;
}> = [
{ name: "Thommy", chonkFactor: 10 },
{ name: "Abu", chonkFactor: 0 },
{ name: "Bärbel", chonkFactor: 1000 },
{ name: "Garfield", chonkFactor: 3000 },
{ chonkFactor: -100 } // a very skinny stray 😿
];
We'll start with an example using map() and filter() that we'll refactor to use flatMap().
Using map() and filter()
const filteredChonkyCatsByName = cats
.filter(cat => cat.chonkFactor > 800 && cat.name != null) // Filtering the chonks
.map(cat => cat.name) // Mapping to an array of names
console.log(filteredChonkyCatsByName); // Output: ['Bärbel', 'Garfield']
In the above code snippet, we have an array of cat objects. We filter and map our cats by chonk factor to get a list of the chonkiest bois.
Using flatMap()
const flatChonkyCatsByName = cats.flatMap(cat => {
return cat.chonkFactor > 800 && cat.name != null ? cat.name : [];
});
console.log(flatChonkyCatsByName); // Output: ['Bärbel', 'Garfield']
In this code snippet, we use flatMap() to directly filter and map the array of cats to an array of names in one go. If the condition is met, we return an array containing the name; otherwise, we return an empty array. The result is the same as achieved with map() and filter(), but the devil is in the detail. Now, if we take a closer look at our name list type, flatMap really shines.
As you might have noticed, our gang of cats includes one nameless, thinned-out stray (poor guy). Since we don't know its name, we filter out undefined names in both snippets. Do our chonky name lists both have the same type? Spoiler: No.
type FilteredChonkyCatsByName = typeof filteredChonkyCatsByName; // (string | undefined)[]
type FlatChonkyCatsByName = typeof flatChonkyCatsByName; // string[]
The reason for this is that TypeScript treats these operations independently. First, filter creates a new array that includes elements passing the specified condition. However, TypeScript's type system does not refine the type of elements based on runtime checks (like cat.name != null in the example). Therefore, after filtering, the type of the array elements remains the same as the original array, including the possibility of undefined values if the array elements were optional or nullable.
Then, when map is applied, it transforms each element into a new form (in this case, extracting the name property). Since the original array could include elements with undefined names, the resulting array from map is typed as (string | undefined)[], reflecting that some elements might not have been transformed because they were undefined to begin with.
flatMap, on the other hand, combines filtering and mapping in a single step. When you return an array from the callback passed to flatMap (like returning cat.name in an array if it exists, or an empty array otherwise), TypeScript understands that you are intentionally excluding certain types of elements (in this case, undefined names). This is because the empty array ([]) signifies the absence of an element for cases that don't meet the condition, effectively filtering them out.
Therefore, flatMap allows TypeScript to infer a more precise type for the resulting array. Since all elements that could potentially be undefined are replaced with empty arrays (and thus filtered out), the resulting array from flatMap is typed as string[], indicating that it only contains strings. This is a more accurate reflection of the runtime behavior, where only defined names are included in the final array.
Bonus TypeScript 5.5 Fun
TypeScript 5.5 improved the relationship between .filter and TypeScript if we apply the filter in the correct order:
// BONUS: Filter TypeScript 5.5 improvements
const filteredChonkyCatsByName55 = cats
.filter(cat => cat.chonkFactor > 800) // Filtering the chonks
.map(cat => cat.name) // Mapping to an array of names
.filter(name => name != null) // filtering the undefined names
type FilteredChonkyCatsByName55 = typeof filteredChonkyCatsByName55; // string[]
The more you know: You may be wondering why the filter has to come after the map function. At the time of writing, TypeScript only seems to do the described refinement for simple cases like typeof name != null, but not for more complex cases where it needs to refine the whole object type. Filtering first will not give us the expected result (yet).
In TypeScript 5.4 and below the type would still be (string | undefined)[] since the type predicate couldn't be infered.
But let's not just take my word for it. Try it out for yourself! Go check out the Example in the TypeScript Playground and see how the inferred types behave in previous TS versions 💡
🤖 Statement about the usage of AI in this article: This article was written by humans (thanks for the feedback Leo, Bene & Johannes!), including the title, concepts, and code samples. However, we used AI to enhance the style of writing.
Typescript 5.5
Array Methods
flatMap
filter
map
Read also
Leonhard, 07/15/2024
User Input Considered Harmful
TypeScript
Web App Development
Best Practices
Full-Stack
Validation
Moritz Jacobs, 01/29/2024
Heading for Greatness — A React Developer's Guide to HTML Headings
HTML Headings
Best Practices
Uberschrift
Accessibility
SEO
Stephan, 12/18/2023
A small contribution towards a better world
Company Culture
Donations
Charity
Corporate Culture