flatMap in TypeScript
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 fromReturning '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 nestedTypeScript 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 intactas 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 assertionfilter 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 flatMap — map 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 flattenedU 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 arrayas 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 lessBoth 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:
- Tuples returned from the callback get unpacked into a union. Use
mapwithas constif you need the positions. - Returning
[]or[value]turnsflatMapinto a filter that narrows correctly, with no non-null assertions and one pass over the array. - Depth beyond one level means chaining
.flat(), andflatonly tracks the depth when you pass a literal.
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.
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