flatMap in TypeScript

September 8, 202611 min read
Requirements:
ArraysFunctions| Unions

Array.prototype.flatMap is a JavaScript array method. It runs a callback over every element, then flattens the result one level deep. If you arrived here from Scala, Java Streams, or RxJS, those are different flatMaps with the same name — this page is about the array method and what TypeScript infers from it.

The runtime behaviour takes one example to explain. The interesting half is the typing: where the element type of the result comes from, why returning a tuple widens, and the one trick that makes flatMap better than filter followed by map.

What flatMap does

Map, then flatten one level. That is the entire feature.

const sentences = ['the quick fox', 'jumped over']
 
const words = sentences.flatMap((sentence) => sentence.split(' '))
// ['the', 'quick', 'fox', 'jumped', 'over'] — string[]
 
const nested = sentences.map((sentence) => sentence.split(' '))
// [['the', 'quick', 'fox'], ['jumped', 'over']] — string[][]

map gives you an array of arrays. flatMap gives you the flat version, and TypeScript tracks the difference: string[] in the first case, string[][] in the second. You never have to write .map(...).flat() and hope the types line up.

The signature, and where the element type comes from

Here is the declaration from the standard library, trimmed to the part that matters:

interface FlatMapExample<T> {
  flatMap<U>(callback: (value: T, index: number, array: T[]) => U | ReadonlyArray<U>): U[]
}

The return type is U[], and U is never written by you — TypeScript infers it from what your callback returns. The U | ReadonlyArray<U> part is what lets you return either a bare value or an array of them from the same callback.

const ids = [1, 2, 3]
 
const bare = ids.flatMap((id) => `id-${id}`) // string[] — U is string
const wrapped = ids.flatMap((id) => [`id-${id}`]) // string[] — U is still string
const empty = ids.flatMap(() => []) // never[] — nothing to infer U from

Returning 'id-1' and returning ['id-1'] land on the same result type. That is the flexibility the union in the signature buys, and it is the foundation of the filter trick further down. If you want the longer version of how a type parameter like U gets filled in, that is generics.

flatMap only flattens one level

One level. Not "until flat".

const deep = [
  [[1, 2], [3]],
  [[4], [5, 6]],
]
 
const once = deep.flatMap((group) => group)
// [[1, 2], [3], [4], [5, 6]] — number[][], still nested

TypeScript reports this honestly as number[][]. There is no flatMapDepth in the language, so when you need to go deeper, chain flat:

const layers = [
  [[1, 2], [3]],
  [[4], [5, 6]],
]
 
const twice = layers.flatMap((group) => group).flat()
// [1, 2, 3, 4, 5, 6] — number[]

flat() and the depth literal

flat takes an optional depth, and its type is unusually clever about it. The depth has to be a literal number for the compiler to track how far it flattened.

const layered = [[[1], [2]], [[3]]]
 
const flatOne = layered.flat() // number[][]
const flatTwo = layered.flat(2) // number[]
 
let depth = 2 // let widens the literal to number
const flatUnknown = layered.flat(depth) // FlatArray<number[][], -1 | 0 | ... | 20>[]

flat(2) works because 2 is a literal type that FlatArray<Arr, D> can recurse on. Store the depth in a let, or in a const annotated as number, and the literal is gone — the compiler no longer knows how many layers you peeled, so it hands back an unresolved FlatArray instead of number[]. Pass the number inline, or keep the literal: const depth = 2 is inferred as 2, not number, so it recurses just as well as the inline call.

The type-level version of this is a good exercise once the runtime side is comfortable: the Flatten and FlattenDepth challenges ask you to build flat and flat(depth) in the type system.

Returning a tuple widens it

This one surprises people. Return a tuple from the callback and the tuple does not survive.

const entries = [
  { key: 'a', count: 1 },
  { key: 'b', count: 2 },
]
 
const pairs = entries.flatMap((entry) => [entry.key, entry.count])
// (string | number)[] — not [string, number][]

Look back at the signature and the reason is obvious. The callback's return is matched against ReadonlyArray<U>, so [string, number] gives U = string | number and the result is (string | number)[]. Flattening is the whole point — the array you return is unpacked, and its positional structure goes with it.

If you wanted an array of pairs, you wanted map:

const keyCounts = [
  { key: 'a', count: 1 },
  { key: 'b', count: 2 },
]
 
const keptPairs = keyCounts.map((entry) => [entry.key, entry.count] as const)
// (readonly [string, number])[] — structure intact

as const is what pins the positions down. Without it, even map gives you (string | number)[][]. The tuple page covers why that widening happens and how to stop it.

flatMap as a type-narrowing filter

Here is the trick worth remembering. Returning [] drops an element and returning [x] keeps it, so flatMap filters and maps in one pass — and unlike filter, it narrows the type properly.

const raw = ['1', 'two', '3']
 
const numbers = raw.flatMap((value) => {
  const parsed = Number(value)
  return Number.isNaN(parsed) ? [] : [parsed]
})
// [1, 3] — number[]

U is inferred from the two branches: never[] contributes nothing, number[] contributes number. The result is number[], with no assertion anywhere.

Compare that with the filter version on optional properties:

type Contact = { name: string; email?: string }
 
const contacts: Contact[] = [{ name: 'Ada', email: 'ada@example.com' }, { name: 'Alan' }]
 
// With filter, the compiler loses the narrowing across the method boundary:
const viaFilter = contacts.filter((c) => c.email).map((c) => c.email!)
//                                                                 ^ non-null assertion required
 
// With flatMap, the narrowing happens inside one callback and holds:
const viaFlatMap = contacts.flatMap((c) => (c.email ? [c.email] : []))
// string[] ✅ no assertion

filter returns Contact[] — the predicate narrowed c.email inside the callback, but that knowledge does not travel to the map that follows. flatMap keeps the check and the use in the same scope, so the string in [c.email] is a real string. You can get the same result with a type predicate on filter, but that means writing and maintaining the predicate. This is free.

Union-typed arrays are a sharp edge

Calling flatMap on a value typed as a union of array types fails, and the error is confusing the first time.

declare const values: string[] | number[]
 
// values.flatMap((v) => [v])
// ❌ TS2349: Each member of the union has signatures, but none of those
//    signatures are compatible with each other.

The problem is not flatMapmap and filter fail the same way. TypeScript will not merge the two call signatures, because a callback that takes string and one that takes number have no common implementation it can trust. The fix is to widen the value before you iterate:

declare const mixedValues: string[] | number[]
 
const widened: (string | number)[] = mixedValues
const labels = widened.flatMap((v) => [String(v)]) // string[] ✅

An array whose elements are a union is fine. It is the union of two array types that breaks. Union types goes into why those two shapes behave so differently.

Async callbacks are not awaited

flatMap is synchronous. Hand it an async callback and you get an array of promises, because a promise is not an array and there is nothing to flatten.

declare function fetchItems(url: string): Promise<string[]>
 
const urls = ['/a', '/b']
 
const pending = urls.flatMap(async (url) => await fetchItems(url))
// Promise<string[]>[] — nothing was awaited, nothing was flattened

U is inferred as Promise<string[]>, and the result type says so plainly. The fix is to do the awaiting yourself and flatten afterwards:

declare function loadItems(url: string): Promise<string[]>
 
async function loadAll(urls: string[]) {
  const settled = await Promise.all(urls.map((url) => loadItems(url)))
  return settled.flat() // string[] ✅
}

Promise.all collapses the promises, then flat collapses the arrays. Two steps, correct types at each one.

Parsing into an object in one pass

The filter trick composes nicely with Object.fromEntries, which is where the readonly-tuple detail starts to pay off. Skip a bad line by returning [], keep a good one by returning a single-entry array:

const rows = ['host=localhost', 'port=5432', 'garbage']
 
const config = Object.fromEntries(
  rows.flatMap((row) => {
    const separator = row.indexOf('=')
    if (separator === -1) return []
    return [[row.slice(0, separator), row.slice(separator + 1)] as const]
  }),
)
// { [k: string]: string } — 'garbage' dropped, no intermediate array

as const matters here. Object.fromEntries wants entries shaped readonly [PropertyKey, T], and without it the inner array widens to string[], which does not fit. The callback also receives the index and the source array as second and third arguments, the same as map, if you need to number the rows or peek at neighbours.

When map or filter reads better

flatMap earns its place when the callback genuinely produces zero, one, or many outputs per input. When it always produces exactly one, map says that out loud and the reader does not have to check whether a [] branch is hiding somewhere.

const temperatures = [12, 18, 21]
 
const asLabels = temperatures.map((t) => `${t}°C`) // ✅ one in, one out
const alsoLabels = temperatures.flatMap((t) => `${t}°C`) // works, but says less

Both produce string[]. The first tells you the array length is unchanged; the second leaves it open. Reserve flatMap for the cases where the count really can change.

The same goes the other way. If you are only dropping elements and not transforming them, a plain filter with a type predicate keeps the intent obvious:

type Job = { id: string; finishedAt?: Date }
 
function isFinished(job: Job): job is Job & { finishedAt: Date } {
  return job.finishedAt !== undefined
}
 
declare const jobs: Job[]
 
const finished = jobs.filter(isFinished) // (Job & { finishedAt: Date })[]

That predicate is worth writing when you need the narrowed object, not just one field off it — flatMap would hand you the field and throw the rest away.

There is also a second thisArg parameter on flatMap, inherited from the older array methods. It rebinds this inside a function callback. With arrow functions, which is what you are almost certainly writing, it does nothing useful — ignore it.

On performance: the array-returning form allocates a small array per element. That is irrelevant at the sizes most code deals with, and readable flatMap beats a hand-rolled loop. If you are genuinely iterating hundreds of thousands of elements in a hot path, a for...of with push will win — measure before you assume you are in that case.

flatMap needs ES2019

If flatMap is missing entirely, this is why:

Property 'flatMap' does not exist on type 'string[]'.
Do you need to change your target library?

flat and flatMap were added in ES2019, so your tsconfig.json needs "target": "es2019" or later, or an explicit "lib" that includes ES2019. This is a types-only problem — every maintained runtime has shipped both methods for years — so bumping lib is usually enough:

{
  "compilerOptions": {
    "target": "es2017",
    "lib": ["ES2019", "DOM"]
  }
}

Summary

flatMap maps and flattens one level, and TypeScript infers the result element type from whatever your callback returns. Three things to carry away:

The spread operator is the other everyday way to flatten one level, and it is worth knowing which one reads better in a given spot. Spread is the better fit when you are merging a known handful of arrays into a literal; flatMap is the better fit when the number of pieces depends on the data you are iterating over.

Share this article

Become a TypeScript Pro

Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges

Practice with Challenges

Put your flatmap in typescript knowledge to the test with these related challenges.

#459Flatten
Medium
#3243FlattenDepth
Medium
#18220Filter
Medium

Related Concepts

Concepts that build on or relate to flatmap in typescript.

The JavaScript Spread Operator in TypeScriptTypeScript TuplesTypeScript GenericsUnion Types