-
Notifications
You must be signed in to change notification settings - Fork 0
VSCode Guide for Using Top level Await in Typescript
13 November 2020
The await
operator is a primary expression in JavaScript that is used for waiting until a JavaScript Promise
object is settled and it can only be used inside an async function
or inside an async function*
generator. Here is a simple TypeScript example:
import * as fs from 'fs' ;
type AsyncIterables < T > = AsyncIterable < T > | AsyncIterableIterator < T > ;
async function toString < T > ( iterable : AsyncIterables < T > )
{
let text = '' ;
for await ( const datum of iterable )
text += < string > < unknown > datum ;
return text ;
}
const document : string = './test.txt' ;
const options : object = { encoding : 'ascii' } ;
const stream = fs.createReadStream( document, options ) ;
toString( stream ).then( console.log ) ;
This example demonstrates the implementation of the toString
asynchronous function that reads from any asynchronous Iterable
type and collects the contents into the text
string variable until there are no more datum in the Iterable
. The toString
asynchronous function resolves the JavaScript Promise
object it initially made when it returns the contents of the text
string variable.
The Node.js fs.createReadStream
method creates an asynchronous read stream for the document ./test.txt
which the toString
asynchronous function consumes until the document has been completely read. When the initial JavaScript Promise
object returned by the toString
asynchronous function is settled, then the JavaScript Promise.then
chaining method makes the result available to the Node.js console.log
method.
The above example is simplistic for this discussion however, realize:
- Instead of reading text from a document, a real world example might be reading from an HTTP stream or a database.
- Instead of simply displaying the results on the console, there could be business logic that needs to happen in the JavaScript
Promise.then
chaining method which might initiate complex nesting that could ultimately lead to a Pyramid of Doom.
Instead of waiting for a JavaScript Promise
object to be settled and using the JavaScript Promise.then
chaining method to handle the result, what if we could rewrite the last line of the example more intuitively as:
console.log( await toString( stream ) ) ;
Unfortunately, at the time of this article, you cannot do that since the JavaScript await
operator can only be used within an async function
or within an async function*
generator. The focus of the ECMAScript TC39 Top-level await
proposal is to allow the use of the JavaScript await
operator outside of an async function
or outside an async function*
generator, with some caveats.
The ECMAScript TC39 Top-level await
proposal has reached Stage 3 status and has been experimentally implemented in both Node.js and TypeScript.
When I initially tried to setup my VSCode environment I kept receiving:
Top-level 'await' expressions are only allowed when the 'module' option is set to 'esnext' or 'system', and the 'target' option is set to 'es2017' or higher.ts(1378)
The TypeScript error is informative however, after changing my tsconfig.json
configuration options to be:
{
"compilerOptions": {
"target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
// Lots of other options removed for clarity...
}
}
TypeScript continued to give me error 1378. After some extensive Googling I was unable to find a resolution for this issue and decided to step back to reevaluate the situation.
It is important to note the caveats in the ECMAScript TC39 Top-level await
proposal. You are only allowed to use the JavaScript await
operator outside of an async function
and outside of an async function*
generator, when it is done inside an ES module. This is what TypeScript error 1378 is telling you. So why did TypeScript still give me error 1378 after I changed my tsconfig.json
configuration options?
The solution is simple, the answer is complicated, but it boils down to a lack of coordination between TypeScript and Node.js. TypeScript transpiles its .ts
language files into .js
JavaScript files which can then be consumed by the JavaScript machinery, e.g., Node.js. Both TypeScript and Node.js need to support the ECMAScript TC39 Top-level await
proposal, which they do. TypeScript has supported the proposal since its v3.8 revision and Node.js has supported the proposal since its v14.8.0 revision.
TypeScript error 1378 was explicit in how to correct the situation, but what about Node.js? There are two significant aspects about Node.js supporting the ECMAScript TC39 Top-level await
proposal:
- Node.js "unflagged" its experimental command line flag
--harmony-top-level-await
. - Node.js, per the proposal, implemented the use the JavaScript
await
operator outside of anasync function
and outside of anasync function*
generator, when it was done inside an ES module.
The "unflagging" of the experimental --harmony-top-level-await
command line flag means that they turned the experimental feature on by default rather than requiring you to enable the feature. It also means that you do not need to change any VSCode build or debug tasks to include that command line flag when running or debugging the TypeScript transpiled files.
The other part is where the issue becomes problematic. Node.js correctly follows the ECMAScript TC39 Top-level await
proposal. ES modules must be placed in JavaScript files ending with a .mjs
file extension. Unfortunately, TypeScript transpiles its language files to JavaScript files ending with a .js
file extension and there is currently no way to allow TypeScript to generate ES modules with an .mjs
file extension.
It might appear that we are at the end of the road without a resolution, but we need to dig deeper into Node.js and its implementation of ES modules. In the Enabling section of the documentation it says:
Node.js treats JavaScript code as CommonJS modules by default. Authors can tell Node.js to treat JavaScript code as ECMAScript modules via the
.mjs
file extension, thepackage.json
"type" field, or the--input-type
flag. See Modules: Packages for more details.
The solution to TypeScript error 1378 involves not only changing the appropriate options in the tsconfig.json
configuration file, but also configuring the appropriate options in the package.json
configuration file:
{
"type": "module",
"engines" : { "node" : ">=14.8.0" },
}
The above snippet is only showing the appropriate options in the package.json
configuration file for clarity. To change Node.js behavior when loading ES modules you must change the type
key to have the value module
so it will load ES modules from either JavaScript files with a .js
or a .mjs
file extension. The engines
key is not strictly required, but since the ECMAScript TC39 Top-level await
proposal was turned on by default in the v14.8.0 revision, it is a dependency of the Node.js project and it should be made explicit.
Unfortunately, after I changed the package.json
configuration options, TypeScript gave me:
'await' expressions are only allowed at the top level of a file when that file is a module, but this file has no imports or exports. Consider adding an empty 'export {}' to make this file a module.ts(1375)
Here is a simple TypeScript example demonstrating error 1375:
console.log( await Promise.resolve( 200 ) ) ;
TypeScript error 1375 is valid. This TypeScript file is not a valid ES module since it does not have any import or export statements. However, when TypeScript transpiles this file to JavaScript and the resulting JavaScript file is run by Node.js, Node.js produces the correct result! The reason is that we changed the appropriate options in the package.json
configuration file and the Node.js behavior changed to treat JavaScript files ending in .js
and .mjs
as ES modules.
Some takeaways from this:
- For the initial TypeScript error 1378, TypeScript could have easily checked the project's
package.json
configuration file and seeing that thetype
key was either missing or not set appropriately could have also indicated that correction in its message. - For the TypeScript error 1375, TypeScript considers this to be an error however, it could have checked the project's
package.json
configuration file and seeing thetype
key was set to the valuemodule
then either reduced this situation to a warning or not given any error since the options in thepackage.json
configuration file are set appropriately for Node.js to consider the transpiled JavaScript as a valid ES module.
To correct TypeScript 1375 error in the above one line example, simply change it to:
export { } ;
console.log( await Promise.resolve( 200 ) ) ;
Now both TypeScript and Node.js are cooperating and living in harmony.
- ECMAScript 2020 Language Specification
- ECMAScript TC39 Top-level
await
Proposal - Mozilla Developer Network JavaScript Guide: Iterators and generators
- Mozilla Developer Network JavaScript Guide: Using promises
- Mozilla Developer Network JavaScript Reference:
async function
statement - Mozilla Developer Network JavaScript Reference:
await
operator - Mozilla Developer Network JavaScript Reference:
Promise
object - Node.js ECMAScript Modules Documentation
- Node.js v14.8.0 Release Notes
- TypeScript v3.8 Release Notes
- Wikipedia Pyramid of Doom article