This TypeScript error occurs when you try to use modern iteration features (for...of loops, array spread, or destructuring) on complex types while targeting older JavaScript environments (ES3/ES5) without enabling the downlevelIteration compiler option. The fix involves enabling downlevelIteration in tsconfig.json or upgrading your compilation target.
The 'downlevelIteration' error appears when TypeScript needs to transpile modern JavaScript iteration features to older ECMAScript versions (ES3 or ES5), but encounters types that require more sophisticated iteration handling. "Downleveling" is TypeScript's term for transpiling to an older version of JavaScript. When targeting ES5 or ES3, TypeScript must convert modern iteration primitives—such as for...of loops, array spread syntax ([...array]), argument spread (fn(...args)), and destructuring—into code that works in environments without native Symbol.iterator support. By default, TypeScript uses a simplified downleveling approach that works for basic arrays but fails for: - Iterators and iterables (Map.values(), Set.entries(), generators) - Strings in for...of loops (which iterate by Unicode code point) - Custom objects implementing Symbol.iterator - Type unions that may include iterables When you enable downlevelIteration, TypeScript emits more accurate helper code that checks for Symbol.iterator at runtime and properly handles all iteration scenarios. This comes at the cost of slightly larger bundle sizes due to the additional helper functions.
The most direct fix is to enable the downlevelIteration compiler option:
{
"compilerOptions": {
"target": "ES5",
"downlevelIteration": true
}
}This tells TypeScript to emit more sophisticated iteration code that properly handles all iterable types. After making this change, recompile your project:
npx tsc
# or
npm run buildThe error should now be resolved. TypeScript will generate helper functions that check for Symbol.iterator and handle iteration correctly for all types.
If you don't need to support very old browsers (IE11 and earlier), consider upgrading your target:
{
"compilerOptions": {
"target": "ES2015", // or "ES6", "ES2016", etc.
"module": "commonjs"
}
}Modern JavaScript environments (ES2015+) have native Symbol.iterator support, so TypeScript doesn't need to downlevel iteration at all. This produces:
- Smaller bundle sizes (no helper functions)
- Faster code (native iteration)
- Simpler output code
Check your browser/runtime requirements before changing:
# ES2015/ES6 supported by:
# - Node.js 6+
# - Chrome 51+
# - Firefox 54+
# - Safari 10+
# - Edge 15+If you can't enable downlevelIteration or upgrade your target, refactor code to use Array.from():
// BEFORE - causes error with ES5 target
const mySet = new Set([1, 2, 3]);
const array = [...mySet]; // Error: downlevelIteration required
// AFTER - works without downlevelIteration
const array = Array.from(mySet);This works because Array.from() is a runtime function (can be polyfilled) rather than syntax that needs transpilation. More examples:
// Map values
const myMap = new Map([["a", 1], ["b", 2]]);
// const values = [...myMap.values()]; // Error
const values = Array.from(myMap.values()); // Works
// Strings (iterate by character)
const text = "hello";
// const chars = [...text]; // Error with for...of on strings
const chars = Array.from(text); // Works
// Custom iterables
function* generator() {
yield 1;
yield 2;
}
// const items = [...generator()]; // Error
const items = Array.from(generator()); // WorksNote: You'll still need Array.from polyfilled for ES3/ES5 environments.
Replace for...of loops with traditional index-based loops:
// BEFORE - requires downlevelIteration
const mySet = new Set(['a', 'b', 'c']);
for (const item of mySet) {
console.log(item);
}
// AFTER - works without downlevelIteration
const items = Array.from(mySet);
for (let i = 0; i < items.length; i++) {
console.log(items[i]);
}
// Or use forEach (if available)
Array.from(mySet).forEach(item => {
console.log(item);
});For Maps:
// BEFORE
const myMap = new Map([["key1", "val1"], ["key2", "val2"]]);
for (const [key, value] of myMap) {
console.log(key, value);
}
// AFTER
myMap.forEach((value, key) => {
console.log(key, value);
});This approach requires no special compiler flags and works in all environments.
If you enable downlevelIteration, also enable importHelpers to avoid duplicating helper code:
{
"compilerOptions": {
"target": "ES5",
"downlevelIteration": true,
"importHelpers": true
}
}Install the tslib package which contains the helper functions:
npm install tslibThis makes TypeScript import iteration helpers from tslib instead of inlining them in every file:
// Without importHelpers - helpers inlined in every file (larger bundle)
var __read = function() { /* helper code */ };
var array = __read(mySet);
// With importHelpers - single shared import (smaller bundle)
import { __read } from "tslib";
var array = __read(mySet);For large projects, this can significantly reduce bundle size.
### How downlevelIteration Works Internally
When downlevelIteration is enabled, TypeScript generates helper functions that implement the ES6 iteration protocol for ES5:
Without downlevelIteration (simple array loop):
// Source
const items = [...myArray];
// Compiled to ES5 (simple, only works for arrays)
var items = myArray.slice();With downlevelIteration (proper iteration protocol):
// Source
const items = [...myIterable];
// Compiled to ES5 (checks for Symbol.iterator)
var __read = function(o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done)
ar.push(r.value);
} catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
} finally { if (e) throw e.error; }
}
return ar;
};
var items = __read(myIterable);The helper checks for Symbol.iterator and uses it if available, falling back to array-like behavior otherwise.
### Spread Operator Behavioral Differences
An important distinction when using spread syntax with downlevelIteration:
const sparseArray = [1, , 3]; // Array with hole at index 1
// Without downlevelIteration (uses .concat or .slice)
const copy1 = [...sparseArray]; // [1, empty, 3] - preserves hole
// With downlevelIteration (uses iterator)
const copy2 = [...sparseArray]; // [1, undefined, 3] - fills holesThe spread syntax with downlevelIteration replaces holes with undefined because the iterator protocol doesn't distinguish between undefined values and holes.
### Performance Considerations
Enabling downlevelIteration has tradeoffs:
Bundle Size Impact:
- Helper functions add ~300-500 bytes per file without importHelpers
- With importHelpers + tslib: ~2KB one-time cost
- For small projects: May increase bundle by 5-10%
- For large projects with importHelpers: <1% increase
Runtime Performance:
- Native iteration (ES6+ target): Fastest
- Simple downleveling (ES5 without flag): Fast for arrays
- downlevelIteration (ES5 with flag): Slower due to protocol checks
Recommendation:
- If targeting modern environments: Use ES2015+ target (no downleveling needed)
- If targeting IE11: Enable downlevelIteration + importHelpers
- If bundle size is critical: Refactor to Array.from() instead
### When Symbol.iterator is Missing
If your runtime doesn't have Symbol.iterator (very old browsers), you'll need a polyfill even with downlevelIteration:
npm install core-js// At entry point
import 'core-js/es/symbol';
import 'core-js/es/array/iterator';
import 'core-js/es/map';
import 'core-js/es/set';Or use a comprehensive polyfill service:
<script src="https://polyfill.io/v3/polyfill.min.js?features=Symbol%2CArray.prototype.iterator"></script>### Library Authorship
If you're writing a library, consider your consumers' needs:
Option 1: Publish ES5 with downlevelIteration
- Widest compatibility
- Consumers don't need special config
- Larger bundle size
Option 2: Publish ES2015+ modules
- Let consumers choose their target
- Smaller published code
- Requires consumers to transpile node_modules (not always configured)
Best practice: Publish both formats:
// package.json
{
"main": "dist/index.js", // ES5 + downlevelIteration
"module": "dist/index.esm.js", // ES2015 modules
"types": "dist/index.d.ts"
}### TypeScript Version Notes
- TypeScript 2.3+ required for downlevelIteration
- Before TypeScript 2.3, no option existed (had to use ES6 target)
- Modern projects (TypeScript 4+) should consider ES2020+ targets instead
Function expression requires a return type
Function expression requires a return type
Value of type 'string | undefined' is not iterable
How to fix "Value is not iterable" in TypeScript
Type 'undefined' is not assignable to type 'string'
How to fix "Type undefined is not assignable to type string" in TypeScript
Type narrowing from typeof check produces 'never'
How to fix "Type narrowing produces never" in TypeScript
Type parameter 'T' has conflicting constraints
How to fix "Type parameter has conflicting constraints" in TypeScript