How to create Node.js modules using Typescript

; Date: June 16, 2019

Tags: Node.JS »»»» TypeScript

To use TypeScript on the Node.js platform, a core thing to learn is how to create a Node.js module using TypeScript. Obviously using TypeScript on Node.js means transpiling the TypeScript code to JavaScript in a form that is useful on Node.js. Any use of TypeScript means transpiling it to JavaScript, tailoring the generated code to the targeted environment. To target TypeScript for client side use in the browser we need one set of compiler options, and to target Node.js we need a different set of compiler options.

Our goal for this article is to carefully look at how to correctly configure the TypeScript compiler to produce code that is useful on Node.js. Anyone using TypeScript must configure the compiler correctly for the target environment.

Choosing which module format to generate

The TypeScript compiler can generate code in several other module formats. For the NodeJS platform our choice of module formats is between CommonJS and ES Modules formats.

At this point of the evolution of NodeJS (version 10.x is LTS with 12.x just released) we are at a crossroads between two module formats. There is a long history where NodeJS modules were written in the CommonJS style. At the same time we are told the ES Modules format is The Future. But on the Node.js platform that is still considered an experimental feature. Hence it is premature to jump in with both feet and publish a module in the ES Modules format.

What's the difference between the two?

In a CommonJS/NodeJS module, an object is exported containing the public interface of the module. Inside the module that object is referred to as module.exports, and externally it is the object returned by the require function. Any of us who are experienced Node.js coders know this module format by heart.

The ES Modules module format is meant to be universal to all ECMAScript environments.

The ES Module format does not export an object. Instead you declare the objects to be exported (e.g. export function func1(param1, param2) { .. }). The exported objects are available to be imported by other modules (e.g. import func1 from "./path/to/module"). It is possible to selectively import things, rather than importing an entire module.

On the NodeJS platform the current behavior is:

  • An ES6 Module can import both ES6 Modules and CommonJS modules via the import statement
  • A CommonJS module can only use require to load a CommonJS module, and cannot load an ES6 Module

For a package to be useful to both ES6 Modules and CommonJS code, it must be implemented in the CommonJS format. Further, while the ES6 Module feature is fairly robust, and it carries some advantages, support for this in 12.x is regarded as an experimental feature. Therefore it seems premature for NodeJS modules to make themselves available as an ES module.

As a result, the current recommendation is to configure the TypeScript compiler to generate CommonJS source from TypeScript source.

Generic TypeScript project setup for Node.js

Now that we've fully verified the module format to use, let's go over project setup. There is a complete project directory available at (github.com) robogeek/typescript-nodejs-quick-start, then look in the registrar directory. That repository was created for the book: (www.amazon.com) Quick Start to using Typescript and TypeORM in Node.js applications. This article is derived from that book.

Create a directory ts-example, then run npm to initialize the project.

$ mkdir ts-example
$ cd ts-example
$ npm init
.. answer questions

That initializes the package.json.

$ npm install @types/node --save-dev
$ npm install typescript ts-node --save-dev

This installs the required modules. The typescript module is the compiler, and ts-node is a convenience program letting us run TypeScript scripts without compiling them. The @types/node module is a collection of useful definitions so that the TypeScript compiler knows about Node.js objects.

This module comes to us through the Definitely Typed collection. That project is a large library of "types" for TypeScript that help us to use libraries with TypeScript that don't directly generate a TypeScript definitions file. A definitions file is kind of what it sounds like, it contains definitions of functions or objects, their parameters or fields, with fully spelled out type definitions.

By using a types module, TypeScript knows the shape of the corresponding library.

Of course the best result comes when a library ships its own TypeScript definitions file inside the library. That way we do not need to separately install the definitions module, and presumably the author of the library can do a better job creating a definitions file than a 3rd party. For example, to use the Express framework with TypeScript we need to separately install the @types/express module.

Create a file named tsconfig.json containing:

{
    "compilerOptions": {
        "lib": [ "es5", "es6", "es7",
                 "es2015", "es2016",
                 "es2017", "es2018",
                 "esnext" ],
        "target": "es2017",
        "module": "commonjs",
        "moduleResolution": "Node",
        "outDir": "./dist",
        "rootDir": "./lib",
        "declaration": true,
        "declarationMap": true,
        "inlineSourceMap": true,
        "inlineSources": true
    }
}

This file is one way to specify TypeScript compiler options.

  • The target line says to output ES2017 code, which we need because that version supports for async/await functions.
  • The module line describes the output module format which will be used, commonjs matching the decision to use the CommonJS module format.
  • The moduleResolution parameter says to look up modules in node_modules directories just like NodeJS does.
  • The outDir parameter says to compile files into the named directory, and the rootDir parameter says to compile the files in the named directory.
  • The declaration and declarationMap parameters says to generate declaration files.
  • The inlineSourceMap and inlineSources say to generate source-map data inside JavaScript source files.

What that means is our source code will be in lib and TypeScript will compile it into dist.

As we said earlier this file configures the TypeScript compiler to output code in CommonJS format. To output ES6 Modules instead we just change the value of the module attribute.

Fleshing out the package.json

While a package.json file was automatically created we must do a little bit further. Add this:

{
    ...
    "main": "dist/index.js",
    "type": "commonjs",
    "types": "./dist/index.d.ts",
    "scripts": {
        "build": "tsc",
        "watch": "tsc --watch",
        "test": "cd test && npm run test"
    },
    ...
}

For the main field we're assuming that lib/index.ts is the main entry point to the module. Remember that we configured the lib directory to contain TypeScript source, and that the dist directory will contain the compiled source.

The type field makes it clear the JavaScript code is in CommonJS format.

The types field declares to the world that this module contains type definitions. It is good form to automatically generate type definitions, which the tsconfig.json file shown earlier does, and then make sure the world knows that type definitions are included.

We've included a build script to run the compiler. The watch script starts a persistent process to automatically watch for file changes and recompile. And we round this off with a test script to support running unit tests in the test subdirectory.

Next step - write code and tests

Your next step is beyond the scope of this particular article, which was to demonstrate how to initialize a TypeScript module for Node.js.