Monday, August 13, 2018

Running ES6 Modules Native in the Browser using TypeScript

ES6 Modules have a clean and intuitive syntax, and it's nice to know that there is now widespread support natively to use them in the browser. The main drawback to date is that not every browser version dating back supports ES6 modules and thus a module/loader bundler is still required for production applications (only one of many reasons). However it can be a lot of work to spin up Webpack or SystemJS just to throw together a small test application. Another use case might be if building an intranet app in a controlled/known environment using a supported browser. Regardless of the reason here is how you can use ES6 modules natively in the browser using TypeScript or JavaScript. (This post will focus on TypeScript, but you can use plain JS too)

1. Create an ES6 module

A module is nothing more than a self-contained set of functionality, executed within their own scope and not on the global scope. We'll use the export keyword to expose that functionality outside the module, and the import keyword to use that exposed functionality in another module.

export class Person {
    getAddress():string{
        return '123 Pine St.';
    }
}


2. Create another ES6 module importing the module created previously

import { Person } from "./Person.js";
export class ContactInfo{
    getContactInfo() {
        const person = new Person();
        const address = person.getAddress();
        console.log(`The address is: ${address}`)
    }
}

Make sure to note that "bare" modules are not supported. This restriction allows for browsers to scale in the future when using module loaders, and allow "bare" modules to contain special meaning or functionality. This syntax below is not supported and you'll get the following error in the browser, even though the application might build correctly:

import { Person } from "Person";
"Uncaught TypeError: Failed to resolve module specifier "Person". Relative references must start with either "/", "./", or "../"."

The correct syntax is to reference the exact file directly. 


3. Configure tsconfig.json

Configure the module type to be "ES6." Using anything else will ultimately yield in errors in the browser at runtime as the other module types are not supported. 

"compilerOptions": {
    "module": "es6"
  }


4. Leverage the "module" type in index.html

As this isn't a classic script, we must identify the file as being a module using the type="module" attribute. Note you can use the async attribute with modules if desired which will execute the script as soon as possible, without a guarantee of order, and also not waiting for the HTML parsing to finish. There is also no need to add the defer attribute as it behaves like this by default; a module script can't block the parser.

<script src="scripts/typescript/Person.js" type="module"></script>
<script src="scripts/typescript/ContactInfo.js" type="module"></script>


5. Run and check for errors

If you see any errors such as "exports is not defined" or "define is not defined" then you probably have the tsconfig.json configured incorrectly and it's instead transpiling to another module type that is unsupported in the browser (i.e. commonjs, amd, etc..) natively without a module loader. If testing the basic functionality above, upon calling getContactInfo() you should see the output in the debugger.


For a full list of browser compatibility on ES6 module support, see the following link:
JavaScript modules via script tag

If you're wondering about the .mjs module file extension support in TypeScript, see the following discussion on GitHub: Support '.mjs' output