Publishing Node modules with TypeScript and ES modules
Tags: node, typescript
Published: 2020-05-15 13:32:35

---
title: Publishing Node modules with TypeScript and ES modules
published: true
date: 2020-04-23 13:00:48 UTC
tags: node,typescript
canonical_url: https://blog.logrocket.com/publishing-node-modules-typescript-es-modules/
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/tiyton5vtigaiiy885of.jpeg
---
**Written by [Jack Franklin](https://blog.logrocket.com/author/jackfranklin/)**✏️

[TypeScript](https://www.typescriptlang.org/) has become a very popular language to write JavaScript in, and with good reason. Its typing system and compiler are able to catch a variety of bugs at compile time before your software has even run, and the additional code editor functionality makes it a very productive environment to be a developer in.

But what happens when you want to write a library or package in TypeScript, yet ship JavaScript so that your end users don’t have to manually compile your code? And how do we author using modern JavaScript features like ES modules whilst still getting all the benefits of TypeScript?

This article aims to solve all these questions and provide you with a setup that’ll let you confidently write and share TypeScript libraries with an easy experience for the consumers of your package.

[![LogRocket Free Trial Banner](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/f760c-1gpjapknnuyhu8esa3z0jga.png?resize=1200%2C280&ssl=1)](https://logrocket.com/signup/)

## Getting started

The first thing we’re going to do is set up a new project. We’re going to create a basic maths package throughout this tutorial — not one that serves any real-world purpose — because it’ll let us demonstrate all the TypeScript we need without getting sidetracked on what the package actually does.

First, create an empty directory and run `npm init -y` to create a new project. This will create your `package.json` and give you an empty project to work on:

```jsx
$ mkdir maths-package
$ cd maths-package
$ npm init -y
```

And now we can add our first and most important dependency: TypeScript!

```jsx
$ npm install --save-dev typescript
```

> At the time of writing the latest version of TypeScript is 3.8.

Once we’ve got TypeScript installed, we can initialize a TypeScript project by running `tsc --init`. `tsc` is short for “TypeScript Compiler” and is the command line tool for TypeScript.

To ensure you run the TypeScript compiler that we just installed locally, you should prefix the command with `npx`. `npx` is a great tool that will look for the command you gave it within your `node_modules` folder, so by prefixing our command, we ensure we’re using the local version and not any other global version of TypeScript that you might have installed.

```jsx
$ npx tsc --init
```

This will create a `tsconfig.json` file, which is responsible for configuring our TypeScript project. You’ll see that the file has hundreds of options, most of which are commented out (TypeScript supports comments in the `tsconfig.json` file). I’ve cut my file down to just the enabled settings, and it looks like this:

```jsx
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true
  }
}
```

We’ll need to make some changes to this config to enable us to publish our package using ES modules, so let’s go through the options now.

## Configuring `tsconfig.json` options

> If you’re ever looking for a comprehensive list of all possible `tsconfig` options, the TypeScript site has you covered with this [handy reference](https://www.typescriptlang.org/v2/en/tsconfig).

Let’s start with `target`. This defines the level of JavaScript support in the browsers you’re going to be serving your code in. If you have to deal with an older set of browsers that might not have all the latest and greatest features, you could set this to `ES2015`. TypeScript will even support `ES3` if you really need maximum browser coverage.

We’ll go for `ES2015` here for this module, but feel free to change this accordingly. As an example, if I was building a quick side project for myself and only cared about the cutting-edge browsers, I’d quite happily set this to `ES2020`.

## Choosing a module system

Next, we have to decide which module system we’ll use for this project. Note that this isn’t which module system we’re going to author in, but which module system TypeScript’s compiler will use when it outputs the code.

What I like to do when publishing modules is publish two versions:

- A modern version with ES modules so that bundling tools can smartly [tree](https://webpack.js.org/guides/tree-shaking/)[–](https://webpack.js.org/guides/tree-shaking/)[shake](https://webpack.js.org/guides/tree-shaking/) away code that isn’t used, and so a browser that supports ES modules can simply import the files
- A version that uses CommonJS modules (the `require` code you’ll be used to if you work in Node) so older build tools and Node.js environments can easily run the code

We’ll look later at how to bundle twice with different options, but for now, let’s configure TypeScript to output ES modules. We can do this by setting the `module` setting to `ES2020`.

Now your `tsconfig.json` file should look like this:

```jsx
{
  "compilerOptions": {
    "target": "ES2015",
    "module": "ES2020",
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true
  }
}
```

## Writing some code

Before we can talk about bundling code, we need to write some! Let’s create two small modules that both export a function and a main entry file for our module that exports all our code.

I like to put all my TypeScript code in a `src` directory because that means we can point the TypeScript compiler directly at it, so I’ll create `src/add.ts` with the following:

```jsx
export const add = (x: number, y:number):number => {
  return x + y;
}
```

And I’ll create `src/subtract.ts`, too:

```jsx
export const subtract = (x: number, y:number):number => {
  return x - y;
}
```

And finally, `src/index.ts` will import all our API methods and export them again:

```jsx
import { add } from './add.js'
import { subtract } from './subtract.js'
export {
  add,
  subtract
}
```

This means that a user can get at our functions by importing just what they need, or by getting everything:

```jsx
import { add } from 'maths-package';

import * as MathsPackage from 'maths-package';
```

Notice that in `src/index.ts` my imports include file extensions. This isn’t necessary if you only want to support Node.js and build tools (such as webpack), but if you want to support browsers that support ES modules, you’ll need the file extensions.

## Compiling with TypeScript

Let’s see if we can get TypeScript compiling our code. We’ll need to make a couple of tweaks to our `tsconfig.json` file before we can do that:

```jsx
{
  "compilerOptions": {
    "target": "ES2015",
    "module": "ES2020",
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./lib",
  },
  "include": [
    "./src"
  ]
}
```

The two changes we’ve made are:

- `compilerOptions.outDir` – this tells TypeScript to compile our code into a directory. In this case, I’ve told it to name that directory `lib`, but you can name it whatever you’d like
- `include` – this tells TypeScript which files we’d like to be included in the compilation process. In our case, all our code sits within the `src` directory, so I pass that in. This is why I like keeping all my TS source files in one folder — it makes the configuration really easy

Let’s give this a go and see what happens! I find when tweaking my TypeScript configuration the approach that works best for me is to tweak, compile, check the output, and tweak again. Don’t be afraid to play around with the settings and see how they impact the final result.

To compile TypeScript, we will run `tsc` and use the `-p` flag (short for “project”) to tell it where our `tsconfig.json` lives:

```jsx
npx tsc -p tsconfig.json
```

If you have any type errors or configuration issues, this is where they will appear. If not, you should see nothing — but notice you have a new `lib` directory with files in it! TypeScript won’t merge any files together when it compiles but will convert each individual module into its JavaScript equivalent.

Let’s look at the three files it’s outputted:

```jsx
// lib/add.js
export const add = (x, y) => {
    return x + y;
};

// lib/subtract.js
export const subtract = (x, y) => {
    return x - y;
};

// lib/index.js
import { add } from './add.js';
import { subtract } from './subtract.js';
export { add, subtract };
```

They look very similar to our input but without the type annotations we added. That’s to be expected: we authored our code in ES modules and told TypeScript to output in that form, too. If we’d used any JavaScript features newer than ES2015, TypeScript would have converted them into ES2015-friendly syntax, but in our case, we haven’t, so TypeScript largely just leaves everything alone.

This module would now be ready to publish onto npm for others to consume, but we have two problems to solve:

1. We’re not publishing any type information in our code. This doesn’t cause breakages for our users, but it’s a missed opportunity: if we publish our types, too, then people using an editor that supports TypeScript and/or people writing their apps in TypeScript will get a nicer experience.
2. Node doesn’t yet support ES modules out of the box. It’d be great to publish a CommonJS version, too, so Node works with no extra effort. [ES module support is coming in Node 13](https://nodejs.org/api/esm.html) and beyond, but it’ll be a while before the ecosystem catches up.

## Publishing type definitions

We can solve the type information issue by asking TypeScript to [emit a declaration file](https://www.typescriptlang.org/v2/en/tsconfig#declaration) alongside the code it writes. This file ends in `.d.ts` and will contain type information about our code. Think of it like source code except rather than containing types and the implementation, it only contains the types.

Let’s add `"declaration": true` to our `tsconfig.json` (in the `"compilerOptions"` part) and run `npx tsc -p tsconfig.json` again.

> Top tip! I like to add a script to my `package.json` that does the compiling so it’s less to type:

```jsx
  "scripts": {
    "tsc": "tsc -p tsconfig.json"
  }
```

> And then I can run `npm run tsc` to compile my code.

You’ll now see that alongside each JavaScript file — say, `add.js` — there’s an equivalent `add.d.ts` file that looks like this:

```jsx
// lib/add.d.ts
export declare const add: (x: number, y: number) => number;
```

So now when users consume our module, the TypeScript compiler will be able to pick up all these types.

## Publishing to CommonJS

The final part of the puzzle is to also configure TypeScript to output a version of our code that uses CommonJS. We can do this by making two `tsconfig.json` files, one that targets ES modules and another for CommonJS. Rather than duplicate all our configuration, though, we can have the CommonJS configuration extend our default and override the `modules` setting.

Let’s create `tsconfig-cjs.json`:

```jsx
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "module": "CommonJS",
    "outDir": "./lib/cjs"
  },
}
```

The important part is the first line, which means this configuration inherits all settings from `tsconfig.json` by default. This is important because you don’t want to have to sync settings between multiple JSON files. We then override the settings we need to change. I update `module` accordingly and then update the `outDir` setting to `lib/cjs` so that we output to a subfolder within `lib`.

At this point, I also update the `tsc` script in my `package.json`:

```jsx
"scripts": {
  "tsc": "tsc -p tsconfig.json && tsc -p tsconfig-cjs.json"
}
```

And now when we run `npm run tsc`, we’ll compile twice, and our `lib` directory will look like this:

```jsx
lib
├── add.d.ts
├── add.js
├── cjs
│   ├── add.d.ts
│   ├── add.js
│   ├── index.d.ts
│   ├── index.js
│   ├── subtract.d.ts
│   └── subtract.js
├── index.d.ts
├── index.js
├── subtract.d.ts
└── subtract.js

1 directory, 12 files
```

This is a bit untidy; let’s update our ESM output to output into `lib/esm` by updating the `outDir` option in `tsconfig.json` accordingly:

```jsx
lib
├── cjs
│   ├── add.d.ts
│   ├── add.js
│   ├── index.d.ts
│   ├── index.js
│   ├── subtract.d.ts
│   └── subtract.js
└── esm
    ├── add.d.ts
    ├── add.js
    ├── index.d.ts
    ├── index.js
    ├── subtract.d.ts
    └── subtract.js

2 directories, 12 files
```

> Feel free to have your own naming conventions or directory structures — this is just what I like to go with, but that doesn’t mean you have to as well!

## Preparing to publish our module

We now have all the parts we need to publish our code to npm. The last step is to tell Node and our users’ preferred bundlers how to bundle our code.

The first property in `package.json` we need to set is `main`. This is what defines our primary entry point. For example, when a user writes `const package = require('maths-package')`, this is the file that will be loaded.

To maintain good compatibility, I like to set this to the CommonJS source since, at the time of writing, that’s what most tools expect by default. So we’ll set this to `./lib/cjs/index.js`.

Next, we’ll set the `module` property. This is the property that should link to the ES modules version of our package. Tools that support this will be able to use this version of our package. So this should be set to `./lib/esm/index.js`.

Next, we’ll add a `files` entry to our `package.json`. This is where we define all the files that should be included when we publish the module. I like to use this approach to explicitly define what files I want included in our final module when it’s pushed to npm.

This lets us keep the size of our module down — we won’t publish our `src` files, for example, and instead publish the `lib` directory. If you provide a directory in the `files` entry, all its files and subdirectories are included by default, so you don’t have to list them all.

> Top tip! If you want to see which files will be included in your module, run `npx pkgfiles` to get a list.

Our `package.json` now has these additional three fields in it:

```jsx
  "main": "./lib/cjs/index.js",
  "module": "./lib/esm/index.js",
  "files": [
    "lib/"
  ],
```

There’s one last step. Because we are publishing the `lib` directory, we need to ensure that when we run `npm publish`, the `lib` directory is up to date. [The npm documentation has a section about how to do just this](https://docs.npmjs.com/misc/scripts#use-cases) — and we can use the `prepublishOnly` script. This script will be run for us automatically when we run `npm publish`:

```jsx
"scripts": {
  "tsc": "tsc -p tsconfig.json && tsc -p tsconfig-cjs.json",
  "prepublish": "npm run tsc"
},
```

> Note that there is also a script called `prepublish`, making it slightly confusing which to choose. [The npm docs mention this](https://docs.npmjs.com/misc/scripts#deprecation-note): `prepublish` is deprecated, and if you want to run code only on publish, you should use `prepublishOnly`.

And with that, running `npm publish` will run our TypeScript compiler and publish the module online! I published the package under `@jackfranklin/maths-package-for-blog-post`, and whilst I don’t recommend you use it, you can [browse the files and have a look](https://unpkg.com/browse/@jackfranklin/maths-package-for-blog-post@0.1.0/). I’ve also uploaded all the code into [CodeSandbox](https://codesandbox.io/s/xisgq) so you can download it or hack with it as you please.

## Conclusion

And that’s it! I hope this tutorial has shown you that getting up and running with TypeScript isn’t quite as daunting as it first appears, and with a bit of tweaking, it’s possible to get TypeScript outputting the many formats you might need with minimal fuss.

* * *
## 200's only ‎✅: Monitor failed and show GraphQL requests in production 

While GraphQL has some features for debugging requests and responses, making sure GraphQL reliably serves resources to your production app is where things get tougher. If you’re interested in ensuring network requests to the backend or third party services are successful, [try LogRocket.](https://www2.logrocket.com/signup-lr)

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/jd2amkhuiynfjf3i33qk.png)

[LogRocket](https://www2.logrocket.com/signup-lr) is like a DVR for web apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. [Start monitoring for free.](https://www2.logrocket.com/signup-lr)
* * *

The post [Publishing Node modules with TypeScript and ES modules](https://blog.logrocket.com/publishing-node-modules-typescript-es-modules/) appeared first on [LogRocket Blog](https://blog.logrocket.com).