# TypeScript `Map` and `Set` Iteration in 2026: Why `downlevelIteration` Still Breaks Production Builds

> Source: <https://dev.to/jsmanifest/typescript-map-and-set-iteration-in-2026-why-downleveliteration-still-breaks-production-44hj>
> Published: 2026-11-09 12:05:56+00:00

`Map` and `Set` Iteration in 2026: Why `downlevelIteration` Still Breaks Production Builds
*This article was written with the assistance of AI, under human supervision and review.*

TypeScript's [`downlevelIteration`](https://www.typescriptlang.org/tsconfig/#downlevelIteration) flag determines how the compiler transpiles `Map`, `Set`, and array iteration when targeting older JavaScript runtimes. Without it, production builds silently produce broken code that crashes on iterables at runtime. The default behavior downlevels `for...of` loops to index-based iteration, which fails on objects that implement `Symbol.iterator` but lack numeric indices.

The flag's confusing name compounds the problem. Developers expect "downlevel" to mean "produce simpler code for older targets," but enabling the flag actually generates more complex helper code that checks for `Symbol.iterator` before falling back to index-based iteration. The flag appears optional until a production build crashes with "Object is not iterable" on a perfectly valid `Map.values()` call.

*Problem flowchart showing downlevelIteration false leading to index-based iteration that crashes on Map*

The correct configuration enables `downlevelIteration` when targeting runtimes below ES2015 and using `Map`, `Set`, or any iterable protocol. The transpiled output includes a `__values` helper that tests for `Symbol.iterator` support before attempting iteration. This prevents the crash but increases bundle size by 400-800 bytes per file that uses iteration.

*Solution flowchart showing downlevelIteration true generating helper code that handles iterables correctly*

`for...of` loops on `Symbol.iterator` object crash at runtime because the default transpilation assumes numeric indices.`target: es5`, but builds targeting ES2015+ still encounter iteration failures in polyfilled environments where iterables exist but the target is set to ES5 for legacy browser support.
TypeScript's terminology inverts the intuitive meaning of "downlevel." The flag name suggests producing simpler code, but [`downlevelIteration: true`](https://www.typescriptlang.org/tsconfig/#downlevelIteration) emits more complex helpers that implement ES2015 iteration semantics in ES5 environments.

When targeting ES5 or earlier, TypeScript must transpile iteration primitives introduced in ES2015: `for...of` loops, array spreads (`[...arr]`), argument spreads (` fn(...args)`), and destructuring iterables. The default behavior produces index-based loops that fail on objects implementing `Symbol.iterator`.

``` js
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) {
  console.log(key, value);
}
```

Without `downlevelIteration`, this transpiles to:

``` js
var map = new Map([['a', 1], ['b', 2]]);
for (var _i = 0, map_1 = map; _i < map_1.length; _i++) {
  var _a = map_1[_i], key = _a[0], value = _a[1];
  console.log(key, value);
}
```

The transpiled code accesses `map.length` and `map[0]`, which do not exist on `Map` objects. The loop never executes. Runtime engines that support `Map` but run ES5 code produce no error and no output, silently skipping the iteration. Older engines that lack `Map` entirely throw "Map is not defined," but that failure occurs earlier during `Map` construction, masking the iteration problem.

*TypeScript downlevelIteration flag configuration example*

With [`downlevelIteration: true`](https://www.typescriptlang.org/tsconfig/#downlevelIteration), TypeScript emits a `__values` helper that checks for `Symbol.iterator` before falling back to index-based iteration:

``` js
var __values = (this && this.__values) || function(o) {
  var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
  if (m) return m.call(o);
  if (o && typeof o.length === "number") return {
    next: function () {
      if (o && i >= o.length) o = void 0;
      return { value: o && o[i++], done: !o };
    }
  };
  throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
};

var map = new Map([['a', 1], ['b', 2]]);
try {
  for (var map_1 = __values(map), map_1_1 = map_1.next(); !map_1_1.done; map_1_1 = map_1.next()) {
    var _a = map_1_1.value, key = _a[0], value = _a[1];
    console.log(key, value);
  }
} catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
  try {
    if (map_1_1 && !map_1_1.done && (_a = map_1.return)) _a.call(map_1);
  }
  finally { if (e_1) throw e_1.error; }
}
```

The helper first tests whether `Symbol.iterator` exists on the object. If present, it calls the iterator method and consumes the result. If absent, it falls back to numeric indexing. The transpiled loop now respects the iteration protocol, correctly iterating `Map` entries.

The confusion arises because "downlevel" typically means "produce simpler code for older targets." Enabling `downlevelIteration` does the opposite: it produces more sophisticated code that handles both iterable and array-like objects. The flag should have been named `strictIteration` or `iteratorHelpers`, but the existing name persists in TypeScript 6.0 despite the ES5 target deprecation.

*Concept diagram showing how downlevelIteration transforms iteration to check Symbol.iterator before falling back to index-based loops*

The runtime failure mode depends on the JavaScript engine and the transpilation target. Modern engines that support `Map` and `Set` execute the broken code without throwing errors. The iteration simply skips all entries, producing silent bugs that pass unit tests running on modern runtimes but fail in production on older browsers.

A production application targeting ES5 for IE11 compatibility experiences the failure when deployed:

``` js
const permissions = new Set(['read', 'write', 'delete']);
const granted = [...permissions];
console.log(granted);
```

Without `downlevelIteration`, the spread operator transpiles to:

``` js
var permissions = new Set(['read', 'write', 'delete']);
var granted = [].concat(permissions);
console.log(granted);
```

The `Array.prototype.concat` method expects array-like objects or primitives. When passed a `Set`, it treats the entire `Set` as a single element. The output is `[Set(3)]`, not `['read', 'write', 'delete']`. The code executes without errors, but the result is wrong.

Unit tests running on Node.js 20 or Chrome 120 pass because those environments execute the original TypeScript source through transpilation pipelines that default to ES2020+ targets. The test suite never exercises the ES5 transpilation. The bug only appears when the production build runs on the intended target runtime.

*Map and Set iteration failure visualization*

The `for...of` loop failure is harder to detect because the loop body never executes. Developers writing iteration logic for `Map` or `Set` assume the loop will run at least once if the collection is non-empty. When the transpiled code silently skips all entries, downstream logic that depends on the iteration produces unexpected results without obvious stack traces pointing to the iteration site.

A real-world example from an e-commerce checkout flow:

``` js
const lineItems = new Map<string, number>();
lineItems.set('item-123', 2);
lineItems.set('item-456', 1);

let total = 0;
for (const [id, quantity] of lineItems) {
  const price = getPriceById(id);
  total += price * quantity;
}

if (total === 0) {
  throw new Error('Cart is empty');
}
```

Without `downlevelIteration`, the `for...of` loop never executes. The `total` remains `0`, and the code throws "Cart is empty" even though the `lineItems` map contains entries. The error message misleads the developer into investigating cart state management when the actual bug is transpilation configuration.

The destructuring assignment inside the loop exacerbates the failure. The transpiled code attempts to destructure `map_1[_i]`, which is `undefined`. Older engines throw "Cannot read property '0' of undefined," providing a clearer diagnostic. Modern engines that support destructuring but execute the ES5 transpilation produce the same error, but the stack trace points to the transpiled output, not the original source, complicating debugging.

For a detailed comparison of how TypeScript handles readonly collections, see the [ReadonlyMap and ReadonlySet in TypeScript](https://jsmanifest.com/readonlymap-readonlyset-typescript) post.

TypeScript 6.0 deprecated `target: es5`, making ES2015 the minimum supported target. The deprecation does not eliminate the need for `downlevelIteration` because production builds still target ES5 through external tooling or continue using the deprecated target with a suppression flag.

| Configuration scenario | downlevelIteration requirement | Reason | 
|---|---|---|
| TypeScript 6.0 with `target: es2015` or higher | Not required | Native `for...of` and`Symbol.iterator` available in target runtime | 
| TypeScript 6.0 with `target: es5` (deprecated) | Required if using `Map` ,`Set` , or iterables | Transpilation still produces index-based loops without the flag | 
| TypeScript 5.x with `target: es5` | Required if using `Map` ,`Set` , or iterables | No deprecation; standard configuration | 
| External compiler (Babel, SWC) with ES5 target | Depends on external tool | TypeScript does not control final output; Babel and SWC handle iteration independently | 

The deprecation warning reads:

```
Option 'target: es5' is deprecated and will stop functioning in a future release. Use 'target: es2015' or higher.
```

Projects that suppress the warning by adding `"ignoreDeprecations": "5.0"` to `tsconfig.json` continue using `target: es5` in TypeScript 6.0. The transpilation behavior remains identical to TypeScript 5.x. The `downlevelIteration` flag still determines whether iteration produces index-based loops or iterator-aware helpers.

External compilers introduce another dimension. A common build pipeline uses TypeScript for type checking only (`"noEmit": true`) and Babel or SWC for transpilation. These tools handle iteration independently:

*Comparison flowchart showing TypeScript 6.0 with target es5 vs. target es2015 handling of downlevelIteration*

Babel's `@babel/preset-env` transpiles iteration based on the `targets` configuration, not TypeScript's `target`. When `targets` includes ES5 browsers, Babel emits its own iteration helpers regardless of `downlevelIteration`. SWC behaves similarly, using its `jsc.target` option to determine transpilation.

A mixed configuration causes confusion:

```
{
  "compilerOptions": {
    "target": "es2015",
    "downlevelIteration": false
  }
}
```

TypeScript emits ES2015 code with native `for...of` loops. Babel then transpiles to ES5, producing index-based loops that fail on iterables. The developer expects `downlevelIteration: false` to be safe because the TypeScript target is ES2015, but the final output still breaks.

The solution requires aligning TypeScript's target with the final output target. When using external transpilers, set TypeScript's `target` to match the highest version the pipeline supports before external transpilation. For type-checking-only workflows, set `target: esnext` and let the external tool handle all transpilation.

The TypeScript team's recommendation in the 6.0 release notes suggests using external compilers for ES5 output rather than fighting the deprecated `target: es5` option. This shifts the iteration problem to Babel or SWC configuration but does not eliminate it. Developers still need to understand how iteration transpiles to ES5 to configure the external tool correctly.

Production failures cluster around three iteration primitives: spread operators, `for...of` loops, and destructuring assignments. Each fails differently when `downlevelIteration` is disabled and the target is ES5.

Array spreads produce wrong results without throwing errors:

``` js
const base = [1, 2, 3];
const extended = [0, ...base, 4];
js
var base = [1, 2, 3];
var extended = [0].concat(base, [4]);
```

For arrays, `concat` works correctly. The problem appears when spreading non-array iterables:

``` js
const uniqueIds = new Set(['a', 'b', 'c']);
const idArray = [...uniqueIds];
```

The transpiled code uses `concat`:

``` js
var uniqueIds = new Set(['a', 'b', 'c']);
var idArray = [].concat(uniqueIds);
```

The result is `[Set(3)]`, not `['a', 'b', 'c']`. The spread operator treats the entire `Set` as one element.

Destructuring assignments fail silently on `Map.entries()`:

``` js
const config = new Map([['timeout', 5000], ['retries', 3]]);
const [[timeoutKey, timeoutValue]] = config;
```

The transpiled code accesses `config[0]`:

``` js
var config = new Map([['timeout', 5000], ['retries', 3]]);
var _a = config[0], timeoutKey = _a[0], timeoutValue = _a[1];
```

The expression `config[0]` is `undefined`. Destructuring `undefined` throws "Cannot read property '0' of undefined." The error message does not mention `Map` or iteration, obscuring the root cause.

*Practical flowchart showing three iteration primitives and their failure modes without downlevelIteration*

A production case from a logging pipeline:

``` js
const logLevels = new Set(['error', 'warn', 'info']);
const filters = [...logLevels].map(level => `level:${level}`);
```

Without `downlevelIteration`, `filters` becomes `["[object Set]"]` instead of `["level:error", "level:warn", "level:info"]`. The logging query breaks, filtering out all logs because the filter string does not match any actual log level.

The `for...of` failure affects React applications that iterate `Map` state:

```
const [items, setItems] = useState(new Map());

return (
  <ul>
    {Array.from(items).map(([id, item]) => (
      <li key={id}>{item.name}</li>
    ))}
  </ul>
);
```

When transpiled to ES5 without `downlevelIteration`, the `Array.from` call fails because `items[Symbol.iterator]` does not exist in the transpiled output's mental model. The component renders an empty list even when `items` contains entries.

The Biome linter detects some iteration patterns that require `downlevelIteration`, but the check is not comprehensive. For more on Biome's capabilities, see the [Biome vs. oxlint Comparison in 2026](https://jsmanifest.com/biome-oxlint-comparison-2026) post.

Enabling `downlevelIteration` requires evaluating the tradeoff between bundle size and correctness. The flag adds 400-800 bytes per file that uses iteration, depending on how many iteration primitives appear in the source.

A migration strategy starts with identifying files that iterate `Map`, `Set`, or non-array iterables. Grep the codebase for `for...of`, spread operators, and destructuring:

```
grep -rn "for (const .* of" src/
grep -rn "\.\.\." src/
grep -rn "const \[.*\] =" src/
```

Files that iterate only arrays do not require `downlevelIteration`. The default index-based transpilation works correctly for arrays because they have numeric indices and a `length` property.

*Migration flowchart showing decision tree for when to enable downlevelIteration*

For projects using `target: es5` in TypeScript 6.0, enable `downlevelIteration` immediately. The bundle size increase is unavoidable when correctness depends on iteration over non-array iterables.

For projects using external transpilers, the configuration depends on the tool:

Babel with `@babel/preset-env`:

```
{
  "presets": [
    ["@babel/preset-env", {
      "targets": { "ie": "11" },
      "useBuiltIns": "usage",
      "corejs": 3
    }]
  ]
}
```

Babel's `useBuiltIns: usage` option automatically includes the iteration helpers when the source code uses iteration primitives. The TypeScript `downlevelIteration` setting has no effect because Babel performs the transpilation.

SWC with `jsc.target: es5`:

```
{
  "jsc": {
    "target": "es5",
    "loose": false
  }
}
```

SWC's `loose: false` setting emits strict iteration helpers that respect `Symbol.iterator`. Setting `loose: true` produces simpler but incorrect transpilation that assumes all iterables are arrays.

A hybrid approach splits the codebase. Modules that use `Map` or `Set` enable `downlevelIteration` via a separate `tsconfig.json`:

```
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "downlevelIteration": true
  },
  "include": ["src/collections/**/*"]
}
```

The base configuration keeps `downlevelIteration: false`, reducing bundle size for modules that iterate only arrays. The build pipeline compiles both configurations and merges the output.

For correlation ID propagation patterns across async boundaries, which often involve iteration over trace contexts, see the [Correlation IDs for AI Agent Tracing](https://jsmanifest.com/correlation-ids-ai-agents) post.

The most pragmatic approach for new projects in 2026 is to avoid the problem entirely by setting `target: es2015` or higher. Modern browsers that support `Map` and `Set` also support native iteration. Legacy browser support increasingly relies on polyfills for missing APIs rather than transpiling syntax. The `downlevelIteration` flag becomes irrelevant when the target runtime includes `Symbol.iterator` natively.

Enabling `downlevelIteration` when iterating only arrays has minimal impact. The `__values` helper checks for `Symbol.iterator` and falls back to index-based iteration when absent, producing the same behavior as the default transpilation. The cost is the helper function's bundle size, typically 400-600 bytes per file.

Yes, but the flag has no effect when the target runtime natively supports `Symbol.iterator`. TypeScript emits native `for...of` loops and spread operators when `target: es2015` or higher, bypassing the iteration helpers entirely. The flag only matters for `target: es5` or `target: es3`.

The default is `false` for backward compatibility and bundle size. Enabling the flag globally increases output size for all files that use iteration, even when iterating arrays. Projects that do not use `Map` or `Set` pay the bundle size cost without gaining correctness. TypeScript prioritizes smaller default output over automatic correctness in edge cases.

TypeScript's compiler does not warn when iteration fails at runtime due to missing `downlevelIteration`. Static analysis tools like Biome can detect some patterns, but comprehensive detection requires runtime testing on the target environment. The most reliable approach is running integration tests on the actual transpiled output in an ES5 environment like IE11 or a headless browser configured to ES5 mode.

The `__values` helper checks for `Symbol.iterator` at runtime. If absent, it falls back to index-based iteration, producing the same behavior as `downlevelIteration: false` for iterables. The difference is the helper throws a clear error ("Symbol.iterator is not defined") instead of silently failing. For `Map` and `Set`, the runtime must polyfill those constructors separately; `downlevelIteration` only handles the iteration protocol.

The `downlevelIteration` flag remains a frequent source of production bugs in 2026 despite ES5 target deprecation. Projects using `Map`, `Set`, or any `Symbol.iterator` implementation must enable the flag when transpiling to ES5, regardless of whether TypeScript or an external tool performs the transpilation. The default behavior produces index-based loops that fail silently on iterables, causing wrong results that pass unit tests on modern runtimes but break in production on legacy browsers.

TypeScript 6.0's ES5 deprecation shifts the problem to external compilers but does not eliminate it. Developers must understand how iteration transpiles across the entire build pipeline, from TypeScript's type checking through Babel or SWC's final output. The bundle size cost of 400-800 bytes per file is unavoidable when correctness depends on iteration over non-array collections.

For new projects, setting `target: es2015` or higher avoids the flag entirely by relying on native iteration support in modern runtimes. Legacy browser support increasingly uses polyfills for missing APIs rather than transpiling syntax. The `downlevelIteration` flag becomes a legacy configuration option, relevant only to projects maintaining IE11 compatibility or transpiling to truly ancient JavaScript runtimes.
