{"slug": "typescript-map-and-set-iteration-in-2026-why-downleveliteration-still-breaks", "title": "TypeScript `Map` and `Set` Iteration in 2026: Why `downlevelIteration` Still Breaks Production Builds", "summary": "A technical writeup explains that TypeScript's downlevelIteration flag, when left disabled for ES5 targets, silently transpiles for...of loops over Map and Set into index-based loops that never execute, producing no error and no output at runtime. Enabling the flag emits a __values helper that checks for Symbol.iterator before falling back to index-based iteration, fixing the crash at a cost of roughly 400-800 bytes per file that uses iteration. The piece notes the flag's name inverts its intuitive meaning, since enabling it generates more complex helper code rather than simpler output.", "body_md": "`Map` and `Set` Iteration in 2026: Why `downlevelIteration` Still Breaks Production Builds\n*This article was written with the assistance of AI, under human supervision and review.*\n\nTypeScript'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.\n\nThe 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.\n\n*Problem flowchart showing downlevelIteration false leading to index-based iteration that crashes on Map*\n\nThe 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.\n\n*Solution flowchart showing downlevelIteration true generating helper code that handles iterables correctly*\n\n`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.\nTypeScript'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.\n\nWhen 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`.\n\n``` js\nconst map = new Map([['a', 1], ['b', 2]]);\nfor (const [key, value] of map) {\n  console.log(key, value);\n}\n```\n\nWithout `downlevelIteration`, this transpiles to:\n\n``` js\nvar map = new Map([['a', 1], ['b', 2]]);\nfor (var _i = 0, map_1 = map; _i < map_1.length; _i++) {\n  var _a = map_1[_i], key = _a[0], value = _a[1];\n  console.log(key, value);\n}\n```\n\nThe 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.\n\n*TypeScript downlevelIteration flag configuration example*\n\nWith [`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:\n\n``` js\nvar __values = (this && this.__values) || function(o) {\n  var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\n  if (m) return m.call(o);\n  if (o && typeof o.length === \"number\") return {\n    next: function () {\n      if (o && i >= o.length) o = void 0;\n      return { value: o && o[i++], done: !o };\n    }\n  };\n  throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\n};\n\nvar map = new Map([['a', 1], ['b', 2]]);\ntry {\n  for (var map_1 = __values(map), map_1_1 = map_1.next(); !map_1_1.done; map_1_1 = map_1.next()) {\n    var _a = map_1_1.value, key = _a[0], value = _a[1];\n    console.log(key, value);\n  }\n} catch (e_1_1) { e_1 = { error: e_1_1 }; }\nfinally {\n  try {\n    if (map_1_1 && !map_1_1.done && (_a = map_1.return)) _a.call(map_1);\n  }\n  finally { if (e_1) throw e_1.error; }\n}\n```\n\nThe 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.\n\nThe 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.\n\n*Concept diagram showing how downlevelIteration transforms iteration to check Symbol.iterator before falling back to index-based loops*\n\nThe 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.\n\nA production application targeting ES5 for IE11 compatibility experiences the failure when deployed:\n\n``` js\nconst permissions = new Set(['read', 'write', 'delete']);\nconst granted = [...permissions];\nconsole.log(granted);\n```\n\nWithout `downlevelIteration`, the spread operator transpiles to:\n\n``` js\nvar permissions = new Set(['read', 'write', 'delete']);\nvar granted = [].concat(permissions);\nconsole.log(granted);\n```\n\nThe `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.\n\nUnit 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.\n\n*Map and Set iteration failure visualization*\n\nThe `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.\n\nA real-world example from an e-commerce checkout flow:\n\n``` js\nconst lineItems = new Map<string, number>();\nlineItems.set('item-123', 2);\nlineItems.set('item-456', 1);\n\nlet total = 0;\nfor (const [id, quantity] of lineItems) {\n  const price = getPriceById(id);\n  total += price * quantity;\n}\n\nif (total === 0) {\n  throw new Error('Cart is empty');\n}\n```\n\nWithout `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.\n\nThe 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.\n\nFor a detailed comparison of how TypeScript handles readonly collections, see the [ReadonlyMap and ReadonlySet in TypeScript](https://jsmanifest.com/readonlymap-readonlyset-typescript) post.\n\nTypeScript 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.\n\n| Configuration scenario | downlevelIteration requirement | Reason | \n|---|---|---|\n| TypeScript 6.0 with `target: es2015` or higher | Not required | Native `for...of` and`Symbol.iterator` available in target runtime | \n| TypeScript 6.0 with `target: es5` (deprecated) | Required if using `Map` ,`Set` , or iterables | Transpilation still produces index-based loops without the flag | \n| TypeScript 5.x with `target: es5` | Required if using `Map` ,`Set` , or iterables | No deprecation; standard configuration | \n| External compiler (Babel, SWC) with ES5 target | Depends on external tool | TypeScript does not control final output; Babel and SWC handle iteration independently | \n\nThe deprecation warning reads:\n\n```\nOption 'target: es5' is deprecated and will stop functioning in a future release. Use 'target: es2015' or higher.\n```\n\nProjects 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.\n\nExternal 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:\n\n*Comparison flowchart showing TypeScript 6.0 with target es5 vs. target es2015 handling of downlevelIteration*\n\nBabel'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.\n\nA mixed configuration causes confusion:\n\n```\n{\n  \"compilerOptions\": {\n    \"target\": \"es2015\",\n    \"downlevelIteration\": false\n  }\n}\n```\n\nTypeScript 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.\n\nThe 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.\n\nThe 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.\n\nProduction 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.\n\nArray spreads produce wrong results without throwing errors:\n\n``` js\nconst base = [1, 2, 3];\nconst extended = [0, ...base, 4];\njs\nvar base = [1, 2, 3];\nvar extended = [0].concat(base, [4]);\n```\n\nFor arrays, `concat` works correctly. The problem appears when spreading non-array iterables:\n\n``` js\nconst uniqueIds = new Set(['a', 'b', 'c']);\nconst idArray = [...uniqueIds];\n```\n\nThe transpiled code uses `concat`:\n\n``` js\nvar uniqueIds = new Set(['a', 'b', 'c']);\nvar idArray = [].concat(uniqueIds);\n```\n\nThe result is `[Set(3)]`, not `['a', 'b', 'c']`. The spread operator treats the entire `Set` as one element.\n\nDestructuring assignments fail silently on `Map.entries()`:\n\n``` js\nconst config = new Map([['timeout', 5000], ['retries', 3]]);\nconst [[timeoutKey, timeoutValue]] = config;\n```\n\nThe transpiled code accesses `config[0]`:\n\n``` js\nvar config = new Map([['timeout', 5000], ['retries', 3]]);\nvar _a = config[0], timeoutKey = _a[0], timeoutValue = _a[1];\n```\n\nThe 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.\n\n*Practical flowchart showing three iteration primitives and their failure modes without downlevelIteration*\n\nA production case from a logging pipeline:\n\n``` js\nconst logLevels = new Set(['error', 'warn', 'info']);\nconst filters = [...logLevels].map(level => `level:${level}`);\n```\n\nWithout `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.\n\nThe `for...of` failure affects React applications that iterate `Map` state:\n\n```\nconst [items, setItems] = useState(new Map());\n\nreturn (\n  <ul>\n    {Array.from(items).map(([id, item]) => (\n      <li key={id}>{item.name}</li>\n    ))}\n  </ul>\n);\n```\n\nWhen 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.\n\nThe 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.\n\nEnabling `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.\n\nA migration strategy starts with identifying files that iterate `Map`, `Set`, or non-array iterables. Grep the codebase for `for...of`, spread operators, and destructuring:\n\n```\ngrep -rn \"for (const .* of\" src/\ngrep -rn \"\\.\\.\\.\" src/\ngrep -rn \"const \\[.*\\] =\" src/\n```\n\nFiles 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.\n\n*Migration flowchart showing decision tree for when to enable downlevelIteration*\n\nFor 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.\n\nFor projects using external transpilers, the configuration depends on the tool:\n\nBabel with `@babel/preset-env`:\n\n```\n{\n  \"presets\": [\n    [\"@babel/preset-env\", {\n      \"targets\": { \"ie\": \"11\" },\n      \"useBuiltIns\": \"usage\",\n      \"corejs\": 3\n    }]\n  ]\n}\n```\n\nBabel'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.\n\nSWC with `jsc.target: es5`:\n\n```\n{\n  \"jsc\": {\n    \"target\": \"es5\",\n    \"loose\": false\n  }\n}\n```\n\nSWC'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.\n\nA hybrid approach splits the codebase. Modules that use `Map` or `Set` enable `downlevelIteration` via a separate `tsconfig.json`:\n\n```\n{\n  \"extends\": \"./tsconfig.base.json\",\n  \"compilerOptions\": {\n    \"downlevelIteration\": true\n  },\n  \"include\": [\"src/collections/**/*\"]\n}\n```\n\nThe base configuration keeps `downlevelIteration: false`, reducing bundle size for modules that iterate only arrays. The build pipeline compiles both configurations and merges the output.\n\nFor 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.\n\nThe 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.\n\nEnabling `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.\n\nYes, 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`.\n\nThe 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.\n\nTypeScript'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.\n\nThe `__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.\n\nThe `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.\n\nTypeScript 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.\n\nFor 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.", "url": "https://wpnews.pro/news/typescript-map-and-set-iteration-in-2026-why-downleveliteration-still-breaks", "canonical_source": "https://dev.to/jsmanifest/typescript-map-and-set-iteration-in-2026-why-downleveliteration-still-breaks-production-44hj", "published_at": "2026-11-09 12:05:56+00:00", "updated_at": "2026-11-09 12:23:54.408610+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["TypeScript"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/typescript-map-and-set-iteration-in-2026-why-downleveliteration-still-breaks", "markdown": "https://wpnews.pro/news/typescript-map-and-set-iteration-in-2026-why-downleveliteration-still-breaks.md", "text": "https://wpnews.pro/news/typescript-map-and-set-iteration-in-2026-why-downleveliteration-still-breaks.txt", "jsonld": "https://wpnews.pro/news/typescript-map-and-set-iteration-in-2026-why-downleveliteration-still-breaks.jsonld"}}