The difference between map() and flatMap() in TypeScript is the shape of the result. The map() method calls a function on every array element and returns a new array with one result per element. The flatMap() method calls the function the same way, but when the function returns an array, it spreads the array’s items into the result, removing one level of nesting. So map() always keeps the length of the original array, while flatMap() can return more elements, fewer elements or the same number.
We use map() to transform each value, such as turning prices into display labels, and flatMap() when one element turns into several values or none, such as splitting sentences into words. Both are standard JavaScript array methods, and TypeScript infers the type of their result. Neither method changes the original array.
The following example runs both methods on the same input. It uses TypeScript 7.0.2 with strict type checks and Node.js 22.
const nums = [1, 2, 3];
const sentences = ["hello world", "good morning"];
// 1. map(): one result per element
const doubled = nums.map((n) => n * 2); // doubled = [2, 4, 6]
// 2. map() keeps arrays returned by the callback
const nested = sentences.map((s) => s.split(" ")); // nested = [["hello", "world"], ["good", "morning"]]
// 3. flatMap(): map, then flatten one level
const words = sentences.flatMap((s) => s.split(" ")); // words = ["hello", "world", "good", "morning"]
// 4. Same result with map() and flat()
const words2 = sentences.map((s) => s.split(" ")).flat(); // words2 = ["hello", "world", "good", "morning"]
// 5. Add elements: two results per element
const pairs = nums.flatMap((n) => [n, n * 10]); // pairs = [1, 10, 2, 20, 3, 30]
// 6. Remove elements: [] drops the element
const evens = nums.flatMap((n) => (n % 2 === 0 ? [n] : [])); // evens = [2]
// 7. Only one level is flattened
const deep = nums.flatMap((n) => [[n]]); // deep = [[1], [2], [3]]
Notice that both methods give the same result when the callback returns a single value. The difference appears when the callback returns an array, because map() keeps it as a nested array, whereas flatMap() merges its items into the result.
Next, we look at each method on its own and compare flatMap() with map() followed by flat(). After that, we filter and map in one pass, check the inferred types and go through the mistakes that show up most often in code reviews.
1. The map() Method: One Result per Element
The map() method takes a callback function, calls it once for each element, and puts each return value in a new array at the same position. The callback receives the element as its first argument, and most callbacks use only that one. The index and the whole array follow as the second and third arguments.
TypeScript infers the type of the new array from the callback’s return type. In step 2 of the next snippet, the callback returns a string, so labels is a string[] even though nums is a number[], and we do not need to write the type ourselves.
const nums = [1, 2, 3, 4];
// 1. Transform each value
const squares = nums.map((n) => n * n); // squares = [1, 4, 9, 16]
// 2. Change the element type: number[] to string[]
const labels = nums.map((n) => "Item " + n); // labels = ["Item 1", "Item 2", "Item 3", "Item 4"]
// 3. Use the index (second callback parameter)
const indexed = nums.map((n, i) => i + ":" + n); // indexed = ["0:1", "1:2", "2:3", "3:4"]
After these calls, nums is still [1, 2, 3, 4], because the map() method always builds a new array.
In real code, map() runs most often on an array of objects, where we use it to pick one property from each object, or to build new objects with changed fields. For example, a booking page fills a dropdown with guest names, so it maps each guest object to its name.
interface Person {
name: string;
age: number;
}
const people: Person[] = [
{ name: "Lokesh", age: 37 },
{ name: "Raj", age: 35 },
{ name: "John", age: 40 },
];
// 1. Pick one property
const names = people.map((p) => p.name); // names = ["Lokesh", "Raj", "John"]
// 2. Build new objects; the originals stay unchanged
const older = people.map((p) => ({ ...p, age: p.age + 1 }));
// older[0] = { name: "Lokesh", age: 38 }, people[0].age = 37
The object literal in the second callback is wrapped in parentheses. Without them, the braces would be read as a function body instead of an object, and the callback would return undefined. The spread operator …p copies the existing fields, and age overrides one of them.
2. The flatMap() Method: Map and Flatten in One Call
The flatMap() method, added in ES2019, runs the callback like map() does. After that, for every result that is an array, flatMap() adds the array’s items to the output one by one instead of adding the array itself, a step called flattening.
Splitting sentences into words shows the difference clearly. The split() method returns an array, so map() produces an array of arrays (string[][]), while flatMap() produces one list of words (string[]).
const sentences = ["hello world", "good morning"];
// 1. map() gives an array of arrays: string[][]
const nested = sentences.map((s) => s.split(" ")); // nested = [["hello", "world"], ["good", "morning"]]
// 2. flatMap() gives one flat array: string[]
const words = sentences.flatMap((s) => s.split(" ")); // words = ["hello", "world", "good", "morning"]

The same pattern works on objects that hold arrays. In the next snippet, each person has a list of hobbies, and flatMap() collects all of them in one array. John has an empty list, so he adds nothing to the result.
const people = [
{ name: "Lokesh", hobbies: ["chess", "cricket"] },
{ name: "Raj", hobbies: ["music"] },
{ name: "John", hobbies: [] },
];
// 1. Collect all hobbies in one array
const hobbies = people.flatMap((p) => p.hobbies); // hobbies = ["chess", "cricket", "music"]
// 2. Mix single values and arrays
const nums = [1, 2, 3].flatMap((n) => (n === 2 ? [n, n] : n)); // nums = [1, 2, 2, 3]
The second call shows that the callback does not have to return an array every time, because flatMap() adds a value that is not an array as it is. In the TypeScript library, the callback type is (value: T, index: number, array: T[]) => U | ReadonlyArray<U> and the result is U[], so mixing number and number[] returns still gives a number[].
3. map() Followed by flat() Compared With flatMap()
Calling flatMap() gives the same result as calling map() and then flat() with depth 1. The MDN documentation describes flatMap() as slightly more efficient than the two calls, because it does not build the intermediate array of arrays.
The two approaches differ only in depth. The flat() method accepts a depth argument, and flat(Infinity) removes all levels. The flatMap() method has no depth parameter and always flattens one level. Its optional second argument is thisArg, the value of this inside the callback, not a depth.
const nums = [1, 2, 3];
// 1. Two calls and one call give the same result
const a = nums.map((n) => [n, n * 10]).flat(); // a = [1, 10, 2, 20, 3, 30]
const b = nums.flatMap((n) => [n, n * 10]); // b = [1, 10, 2, 20, 3, 30]
// 2. flatMap() removes only one level of nesting
const c = nums.flatMap((n) => [[n]]); // c = [[1], [2], [3]]
// 3. flat() takes a depth
const deep = [1, [2, [3, [4]]]];
const one = deep.flat(); // one = [1, 2, [3, [4]]]
const two = deep.flat(2); // two = [1, 2, 3, [4]]
const all = deep.flat(Infinity); // all = [1, 2, 3, 4]
When the callback can return arrays nested two or more levels deep, we use map() followed by flat(depth). In all other cases, flatMap() is shorter and does less work.
4. Filtering and Mapping in a Single Pass
Because flatMap() accepts an empty array as a result, the callback can decide for each element whether to keep it.
- Returning [] removes the element.
- Returning [value] keeps one result.
- Returning [a, b] adds two results.
Returning arrays of different lengths makes flatMap() a combined filter() and map().
The combined form helps when the filter and the transformation do the same work. For example, a CSV import reads quantities as text and must skip rows where the quantity is not a number. In the next snippet, the filter() version converts each string with Number() to test it and converts it a second time in map(). The flatMap() version converts each string once.
const inputs = ["1", "x", "3"];
// 1. filter() then map(): two passes, Number() called twice
const n1 = inputs.filter((s) => !Number.isNaN(Number(s))).map((s) => Number(s)); // n1 = [1, 3]
// 2. flatMap(): one pass, [] drops the element
const n2 = inputs.flatMap((s) => {
const n = Number(s);
return Number.isNaN(n) ? [] : [n];
}); // n2 = [1, 3]
The same idea removes undefined values and fixes the element type in one call. An array such as [10, undefined, 30] has the type (number | undefined)[]. Since TypeScript 5.5, the compiler infers a type predicate from a callback like s => s !== undefined, so filter() returns number[]. The shortcut filter(Boolean) does not get this inference, and its result keeps undefined in the type.
const scores = [10, undefined, 30]; // (number | undefined)[]
// 1. filter() with a check narrows the type
const s1 = scores.filter((s) => s !== undefined); // s1: number[] = [10, 30]
// 2. filter(Boolean) does not narrow
const s2 = scores.filter(Boolean); // s2: (number | undefined)[] = [10, 30]
// 3. flatMap() narrows too
const s3 = scores.flatMap((s) => s ?? []); // s3: number[] = [10, 30]
The ?? operator gives the right-hand value when the left-hand value is null or undefined, so the callback returns the score or an empty array. The shortcut filter(Boolean) has a second problem, because it also removes falsy values such as 0 and “”. For removing only missing values, the !== undefined check is the clearest choice, and flatMap() is the better choice when the same callback must also transform the value.
5. Differences Between map() and flatMap()
In the result type row, T is the element type of the original array and U is the type the callback returns.
| Point | map() | flatMap() |
|---|---|---|
| Result length | Always the same as the original array | Can be shorter, longer, or the same |
| Callback returns an array | The array is stored as one element | Its items are added to the result |
| Flattening | None | One level only |
| TypeScript result type | U[]; an array result gives U[][] | U[] when the callback returns U or U[] |
| Remove an element | Not possible; use filter() first | Return [] |
| Second argument | thisArg | thisArg (not a depth) |
| Added in | ES5 | ES2019; needs lib ES2019 or later |
| Typical use | Transform each value or object | Split, expand, or filter and map at once |
As a rule, we use map() when every element produces one result, which is the most common case. We use flatMap() when an element can produce zero results or several, or when the callback already returns an array that we want merged.
6. Common Mistakes With map() and flatMap()
Most bugs with these methods come from the callback, not from the method itself. The first one is a compile error in TypeScript, whereas the next two compile without any warning.
A callback with braces needs a return statement, because without it the callback returns undefined and the result type becomes void[]. When the variable has a type annotation, the compiler catches the mistake.
const nums = [1, 2, 3];
const doubled: number[] = nums.map((n) => { n * 2; });
src/err.ts(2,7): error TS2322: Type 'void[]' is not assignable to type 'number[]'.
Type 'void' is not assignable to type 'number'.
The fix is to write return n * 2; inside the braces, or to drop the braces, as in (n) => n * 2. When tsconfig.json sets lib or target below ES2019, flatMap() itself is missing, and tsc reports error TS2550 with the message “Property ‘flatMap’ does not exist on type ‘string[]’. Do you need to change your target library? Try changing the ‘lib’ compiler option to ‘es2019’ or later.”
The other two mistakes compile and fail at runtime.
const nums = [1, 2, 3];
// 1. parseInt() receives the index as the radix
const wrong = ["1", "2", "3"].map(parseInt); // wrong = [1, NaN, NaN]
const right = ["1", "2", "3"].map((s) => parseInt(s, 10)); // right = [1, 2, 3]
// 2. An async callback returns promises
const promises = nums.map(async (n) => n * 2); // Promise<number>[]
const values = await Promise.all(promises); // values = [2, 4, 6]
Passing parseInt as the callback means map() calls parseInt(“2”, 1) and parseInt(“3”, 2), because the index arrives as the second argument, which parseInt() reads as the radix. TypeScript accepts it because the radix parameter is an optional number. An async callback always returns a Promise, so the result is an array of promises; Promise.all() waits for all of them and returns the values in order.
7. Running the map() and flatMap() Examples
The array-map-vs-flatmap folder on GitHub keeps each section’s snippets in their own file under src, and src/index.ts prints every value from the snippet comments. The project pins TypeScript 7.0.2, compiles against the ES2024 library, and runs on Node.js 22 or later.
npm install
npm start
The npm start script compiles the code with tsc and runs dist/src/index.js after that. The file for section 6 uses top-level await, which works because the project is an ES module (“type”: “module” in package.json).
8. Conclusion
The map() method turns each element of a TypeScript array into one result, so the output always has the same length as the input. The flatMap() method lets each element produce any number of results, including none, and merges returned arrays one level deep. We use map() for plain transformations and flatMap() for splitting, expanding, or filtering and mapping in one call. When the nesting is deeper than one level, we combine map() with flat(depth).
9. References
The MDN pages describe the runtime behavior of each method, and the TypeScript 5.5 release notes explain the inferred type predicates used with filter().
- MDN: Array.prototype.map()
- MDN: Array.prototype.flatMap()
- MDN: Array.prototype.flat()
- TypeScript 5.5 release notes: Inferred type predicates
Happy Learning !!