Marius Schulz
Marius Schulz
Front End Engineer

Downlevel Iteration for ES3/ES5 in TypeScript

TypeScript 2.3 introduced a new --downlevelIteration flag that adds full support for the ES2015 iteration protocol for ES3 and ES5 targets. for...of-loops can now be downlevel-compiled with correct semantics.

#Iterating over Arrays Using for...of

Let's assume this brief tsconfig.json file for the following TypeScript code examples. The only option we configure in the beginning is our ECMAScript language target — in this case, ES5:

{
  "compilerOptions": {
    "target": "es5"
  }
}

Check out the following index.ts file. Nothing fancy, just an array of numbers and an ES2015 for...of-loop that iterates over the array and outputs every number:

const numbers = [4, 8, 15, 16, 23, 42];

for (const number of numbers) {
  console.log(number);
}

We can execute the index.ts file directly without running it through the TypeScript compiler first because it doesn't contain any TypeScript-specific syntax:

$ node index.ts
4
8
15
16
23
42

Let's now compile the index.ts file into index.js:

$ tsc -p .

Looking at the emitted JavaScript code, we can see that the TypeScript compiler generated a traditional index-based for-loop to iterate over the array:

var numbers = [4, 8, 15, 16, 23, 42];
for (var _i = 0, numbers_1 = numbers; _i < numbers_1.length; _i++) {
    var number = numbers_1[_i];
    console.log(number);
}

If we run this code, we can quickly see that it works as intended:

$ node index.js
4
8
15
16
23
42

The observable output of running node index.ts and node.index.js is identical, just as it should be. This means we haven't changed the behavior of the program by running it through the TypeScript compiler. Good!

#Iterating over Strings Using for...of

Here's another for...of-loop. This time, we're iterating over a string rather than an array:

const text = "Booh! 👻";

for (const char of text) {
  console.log(char);
}

Again, we can run node index.ts directly because our code only uses ES2015 syntax and nothing specific to TypeScript. Here's the output:

$ node index.ts
B
o
o
h
!

👻

Now it's time to compile index.ts to index.js again. When targeting ES3 or ES5, the TypeScript compiler will happily generate an index-based for-loop for the above code:

var text = "Booh! 👻";
for (var _i = 0, text_1 = text; _i < text_1.length; _i++) {
  var char = text_1[_i];
  console.log(char);
}

Unfortunately, the emitted JavaScript code behaves observably differently from the original TypeScript version:

$ node index.js
B
o
o
h
!


The ghost emoji — or the code point U+1F47B, to be more precise — consists of the two code units U+D83D and U+DC7B. Because indexing into a string returns the code unit (rather than the code point) at that index, the emitted for-loop breaks up the ghost emoji into its individual code units.

On the other hand, the string iteration protocol iterates over each code point of the string. This is why the output of the two programs differs. You can convince yourself of the difference by comparing the length property of the string and the length of the sequence produced by the string iterator:

const ghostEmoji = "\u{1F47B}";

console.log(ghostEmoji.length); // 2
console.log([...ghostEmoji].length); // 1

Long story short: iterating over strings using a for...of-loop doesn't always work correctly when targeting ES3 or ES5. This is where the new --downlevelIteration flag introduced with TypeScript 2.3 comes into play.

#The --downlevelIteration Flag

Here's our index.ts from before again:

const text = "Booh! 👻";

for (const char of text) {
  console.log(char);
}

Let's now modify our tsconfig.json file and set the new downlevelIteration compiler option to true:

{
  "compilerOptions": {
    "target": "es5",
    "downlevelIteration": true
  }
}

If we run the compiler again, the following JavaScript code is emitted:

var __values = (this && this.__values) || function (o) {
    var m = typeof Symbol === "function" && o[Symbol.iterator], i = 0;
    if (m) return m.call(o);
    return {
        next: function () {
            if (o && i >= o.length) o = void 0;
            return { value: o && o[i++], done: !o };
        }
    };
};
var text = "Booh! 👻";
try {
    for (var text_1 = __values(text), text_1_1 = text_1.next(); !text_1_1.done; text_1_1 = text_1.next()) {
        var char = text_1_1.value;
        console.log(char);
    }
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
    try {
        if (text_1_1 && !text_1_1.done && (_a = text_1.return)) _a.call(text_1);
    }
    finally { if (e_1) throw e_1.error; }
}
var e_1, _a;

As you can see, the generated code is a lot more elaborate than a simple for-loop. This is because it contains a proper implementation of the iteration protocol:

  • The __values helper function looks for a [Symbol.iterator] method and calls it if it was found. If not, it creates a synthetic array iterator over the object instead.
  • Instead of iterating over each code unit, the for-loop calls the iterator's next() method until it is exhausted, in which case done is true.
  • To implement the iteration protocol according to the ECMAScript specification, try/catch/finally blocks are generated for proper error handling.

If we now execute the index.js file again, we get the correct output:

$ node index.js
B
o
o
h
!

👻

Note that you still need a shim for Symbol.iterator if your code is executed in an environment that doesn't natively define this symbol, e.g. an ES5 environment. If Symbol.iterator is not defined, the __values helper function will be forced to create a synthetic array iterator that doesn't follow the proper iteration protocol.

#Using Downlevel Iteration with ES2015 Collections

ES2015 added new collection types such as Map and Set to the standard library. In this section, I want to look at how to iterate over a Map using a for...of-loop.

In the following example, I create a mapping from numeric digits to their respective English names. I initialize a Map with ten key-value pairs (represented as two-element arrays) in the constructor. Afterwards, I use a for...of-loop and an array destructuring pattern to decompose the key-value pairs into digit and name:

const digits = new Map([
  [0, "zero"],
  [1, "one"],
  [2, "two"],
  [3, "three"],
  [4, "four"],
  [5, "five"],
  [6, "six"],
  [7, "seven"],
  [8, "eight"],
  [9, "nine"]
]);

for (const [digit, name] of digits) {
  console.log(`${digit} -> ${name}`);
}

This is perfectly valid ES2015 code which runs as expected:

$ node index.ts
0 -> zero
1 -> one
2 -> two
3 -> three
4 -> four
5 -> five
6 -> six
7 -> seven
8 -> eight
9 -> nine

However, the TypeScript compiler is unhappy, saying that it cannot find Map:

TypeScript compile-time error: Cannot find name 'Map'.

This is because we're targeting ES5, which doesn't implement the Map collection. How would we make this code compile, assuming we have provided a polyfill for Map so that the program works at run-time?

The solution is to add the "es2015.collection" and "es2015.iterable" values to the lib option within our tsconfig.json file. This tells the TypeScript compiler that it can assume to find ES2015 collection implementations and the Symbol.iterator symbol at run-time. Once you explicitly specify the lib option, however, its defaults no longer apply. Therefore, you should add "dom" and "es5" in there as well so that you can access other standard library methods.

Here's the resulting tsconfig.json:

{
  "compilerOptions": {
    "target": "es5",
    "downlevelIteration": true,
    "lib": [
      "dom",
      "es5",
      "es2015.collection",
      "es2015.iterable"
    ]
  }
}

Now, the TypeScript compiler no longer complains and emits the following JavaScript code:

var __values = (this && this.__values) || function (o) {
    var m = typeof Symbol === "function" && o[Symbol.iterator], i = 0;
    if (m) return m.call(o);
    return {
        next: function () {
            if (o && i >= o.length) o = void 0;
            return { value: o && o[i++], done: !o };
        }
    };
};
var __read = (this && this.__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 digits = new Map([
    [0, "zero"],
    [1, "one"],
    [2, "two"],
    [3, "three"],
    [4, "four"],
    [5, "five"],
    [6, "six"],
    [7, "seven"],
    [8, "eight"],
    [9, "nine"]
]);
try {
    for (var digits_1 = __values(digits), digits_1_1 = digits_1.next(); !digits_1_1.done; digits_1_1 = digits_1.next()) {
        var _a = __read(digits_1_1.value, 2), digit = _a[0], name_1 = _a[1];
        console.log(digit + " -> " + name_1);
    }
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
    try {
        if (digits_1_1 && !digits_1_1.done && (_b = digits_1.return)) _b.call(digits_1);
    }
    finally { if (e_1) throw e_1.error; }
}
var e_1, _b;

Try it out for yourself — this code prints the correct output.

There's one more thing we should take care of, though. The generated JavaScript code now includes two helper functions, __values and __read, which significantly blow up the code size. Let's try to bring that down.

#Reducing Code Size with --importHelpers and tslib

In the code example above, the __values and __read helper functions were inlined into the resulting JavaScript code. This is unfortunate if you're compiling a TypeScript project with multiple files. Every emitted JavaScript file will contain all helpers necessary to execute that file, resulting in much bigger code!

In a typical project setup, you'll use a bundler such as webpack to bundle together all your modules. The bundle that webpack generates will be unnecessarily big if it contains a helper function more than once.

The solution is to use the --importHelpers compiler option and the tslib npm package. When specified, --importHelpers will cause the TypeScript compiler to import all helpers from tslib. Bundlers like webpack can then inline that npm package only once, avoiding code duplication.

To demonstrate the effect of --importHelpers, I'll first turn our index.ts file into a module by exporting a function from it:

const digits = new Map([
  [0, "zero"],
  [1, "one"],
  [2, "two"],
  [3, "three"],
  [4, "four"],
  [5, "five"],
  [6, "six"],
  [7, "seven"],
  [8, "eight"],
  [9, "nine"]
]);

export function printDigits() {
  for (const [digit, name] of digits) {
    console.log(`${digit} -> ${name}`);
  }
}

Now we need to modify our compiler configuration and set importHelpers to true. Here's our final tsconfig.json file:

{
  "compilerOptions": {
    "target": "es5",
    "downlevelIteration": true,
    "importHelpers": true,
    "lib": [
      "dom",
      "es5",
      "es2015.collection",
      "es2015.iterable"
    ]
  }
}

This is what the resulting JavaScript code looks like after running it through the compiler:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var digits = new Map([
    [0, "zero"],
    [1, "one"],
    [2, "two"],
    [3, "three"],
    [4, "four"],
    [5, "five"],
    [6, "six"],
    [7, "seven"],
    [8, "eight"],
    [9, "nine"]
]);
function printDigits() {
    try {
        for (var digits_1 = tslib_1.__values(digits), digits_1_1 = digits_1.next(); !digits_1_1.done; digits_1_1 = digits_1.next()) {
            var _a = tslib_1.__read(digits_1_1.value, 2), digit = _a[0], name_1 = _a[1];
            console.log(digit + " -> " + name_1);
        }
    }
    catch (e_1_1) { e_1 = { error: e_1_1 }; }
    finally {
        try {
            if (digits_1_1 && !digits_1_1.done && (_b = digits_1.return)) _b.call(digits_1);
        }
        finally { if (e_1) throw e_1.error; }
    }
    var e_1, _b;
}
exports.printDigits = printDigits;

Notice that the code no longer contains inlined helper functions. Instead, the tslib package is required at the beginning.

And there you go! Spec-compliant, downlevel-compiled for...of-loops, full support for the iteration protocol, and no redundant TypeScript helpers.

This article and 44 others are part of the TypeScript Evolution series. Have a look!