Overview
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:
|
|
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:
|
|
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:
|
|
Let’s now compile the index.ts
file into index.js
:
|
|
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:
|
|
If we run this code, we can quickly see that it works as intended:
|
|
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:
|
|
Again, we can run node index.ts
directly because our code only uses ES2015 syntax and nothing specific to TypeScript. Here’s the output:
|
|
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:
|
|
Unfortunately, the emitted JavaScript code behaves observably { 显著地 } differently from the original TypeScript version:
|
|
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:
|
|
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:
|
|
Let’s now modify our tsconfig.json
file and set the new downlevelIteration
compiler option to true
:
|
|
If we run the compiler again, the following JavaScript code is emitted:
|
|
As you can see, the generated code is a lot more elaborate { You use elaborate to describe something that is very complex because it has a lot of different parts } 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’snext()
method until it is exhausted { 耗尽的,枯竭的 }, in which casedone
istrue
. - 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:
|
|
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
:
|
|
This is perfectly valid ES2015 code which runs as expected:
|
|
However, the TypeScript compiler is unhappy, saying that it cannot find 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
:
|
|
Now, the TypeScript compiler no longer complains and emits the following JavaScript code:
|
|
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:
|
|
Now we need to modify our compiler configuration and set importHelpers
to true
. Here’s our final tsconfig.json
file:
|
|
This is what the resulting JavaScript code looks like after running it through the compiler:
|
|
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.