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 πββ¬.
We'll start with an example using map() and filter() that we'll refactor to use flatMap().
Using map() and filter()
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()
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.
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:
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
Francesca, Ricarda, 11/21/2024
Top 10 Mistakes to Avoid When Building a Digital Product
MVP development
UX/UI design
product vision
agile process
user engagement
product development
Leonhard, 10/22/2024
Strategies to Quickly Explore a New Codebase
Web App Development
Consulting
Audit
Leonhard, 07/15/2024
User Input Considered Harmful
TypeScript
Web App Development
Best Practices
Full-Stack
Validation