TypeScript `Map` and `Set` Iteration in 2026: Why `downlevelIteration` Still Breaks Production Builds 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. 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