Setting up Typescript with ES6 modules for NodeJS

This post is more of a reference for myself, but I hope you find it useful too. Recently I went through the trouble of setting up a NodeJS project to handle Redux with Typescript, and there are a few quirks along the way. Here’s the steps to make it happen without going for a deep-dive through module resolution documentation:

  1. Initialise your NPM module and Git repo as you usually would

  2. Ensure that Typescript is installed globally via NPM and accessible as such

This is where it gets non-trivial.

Firstly, you’re no longer just going to have a lib or src folder that your index.js includes to run; you’re going to have a folder with your source .ts files, and a folder that the Typescript transpiler, tsc, builds the resulting validated JavaScript to. Exact naming isn’t essential, just go for what’s intuitive. For me, that’s src for the Typescript, and lib for the JS.

It’s worth updating your package.json file to now reflect your new code entrypoint, by changing the main key to point to your transpiled code directory.

The rest is all in how you set up your project-level tsconfig.json such that the Typescript transpiler builds correct NodeJS compatible files. Here’s the result of half an hour of googling:

{
    "compilerOptions": {
        "outDir": "./lib",
        "allowJs": true,
        "target": "es2018",
        "moduleResolution": "node",
        "module": "commonjs",
        "esModuleInterop": true
    },
    "include": [
        "./src/*",
        "./src/**/*"
    ]
}

Let’s break this down:

outDir: The relative output path for transpiled JS from the package path

allowJS: A boolean flag on whether or not to include JS files found in your source directory into the build, rather than just building the Typescript files found.

target: This is actually a string value from a set, so a de-facto enum, of what ECMAScript version to build to. For compatibility purposes this is great; for Node we build to ES2018 since that’s what the latest LTS (at the time of writing, 13.7.0) can support.

moduleResolution: Another de-facto enumerable. This one just specifies what process tsc will follow to import modules into each other; NodeJS has a specific method outlined in their documentation.

module: This one refers to the type of JS module we expect to transpile to. Although we’ll be using ES6 import syntax in our source Typescript code, NodeJS expects CommonJS-style require() syntax.

esModuleInterop: A boolean, and this one’s some secret sauce. This basically loosens the strictness on ES6 imports, allowing you to import an entire module, despite the fact that no default imports have been specified in that module. This is the difference between getting an import error on code like this:

import myModule from './mymodule'

and code like this:

import { myFunction } from './mymodule'

The Typescript documentation is well-written and goes over all the compiler options in far more depth if you’re interested.

Once this is all set up, you’re done! You should be able to run tsc from the command-line, see your code build, and then run node . from your package root to run it just as you would otherwise.

Hope this helped, and feel free to reach out over email.