Skip to main content
← Back to the liblab blog

TypeScript npm packages done right: tutorial with examples

Publishing packages to NPM is not a particularly difficult challenge by itself. However, configuring your TypeScript project for success might be. Will your package work on most projects? Will the users have type-hinting and autocompletion? Will it work with ES Modules (ESM) and CommonJS (CJS) style imports?

After reading this post, you will understand how to make your TypeScript package more accessible and usable in any (or most) JavaScript and TypeScript projects, including browser support!

Creating a TypeScript Project

Chances are that if you are reading this, you already have a TypeScript project set up. If you do, you might want to skip to the next steps or stay around to check for discrepancies.

Let's start by creating our base Node.js project and adding TypeScript as a development dependency:

npm init -y
npm install typescript --save-dev

You likely want to structure your code inside a src folder. So let's create your package's entry point inside of it:

mkdir src
touch src/index.ts

Now, Node.js and browsers don't understand TypeScript, so we need to set up tsc (TypeScript compiler) to compile our TypeScript code to JavaScript. Let's add a tsconfig.json file to our project by running:

npx tsc --init

If we run npx tsc now, it will scan our folder and create .js files in the same directories as our .ts files (which is not desirable). Let's add better configuration before we run that and make a mess.

Add the following lines to tsconfig.json:

{
"compilerOptions": {
// ... Other options
"rootDir": "./src", // Where to look for our code
"outDir": "./dist", // Where to place the compiled JavaScript
}

Let's also add a "build" script to our package.json:

{
"scripts": {
"build": "tsc"
}
}

If we run npm run build now, a new dist folder will appear with the compiled JavaScript. If you're using Git, make sure to add the dist folder to your .gitignore.

Setting up tsc for Optimal Developer Experience

We can already compile our TypeScript to JavaScript. However, if you publish it to npm as is, you'll only be able to use it seamlessly in other JavaScript projects. Also, the default target configuration is "es2016," and modern browsers only support up to "es2015." So let's fix that!

First, let's change our target to es2015 (or es6 since they're the same). esModuleInterop is true by default. Let's leave it as is since it increases compatibility by allowing ESM-style imports.

We are all using TypeScript for a reason: types! But if you build and ship your package right now, no types will be shipped with it. Let's fix that by setting declaration to true. This will generate declaration files (.d.ts) alongside our .js files. With that alone, your package will be usable in TypeScript projects from the get-go and provide type hints even in JavaScript projects.

The declaration files already go a long way in improving support and developer experience. However, we can go further by adding declarationMap. With that, sourcemaps (.d.ts.map) will be generated to map our declaration files (.d.ts) to our original TypeScript source code (.ts). This means that code editors can go to the original TypeScript code when using "Go to definition," instead of the compiled JavaScript files.

While we're at it, sourceMap will add sourcemap files (.js.map) that allow debuggers and other tools to display the original TypeScript source code when actually working with the emitted JavaScript files.

Using declarationMap and/or sourceMap means we also need to publish our source code with the package to npm.

With all that, here is our final tsconfig.json file:

{
"compilerOptions": {
"target": "es2015",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"rootDir": "./src",
"outDir": "./dist",
"sourceMap": true,
"declaration": true,
"declarationMap": true,
}
}

package.json

Things are much simpler around here. We need to specify the entry point of our package when users import it. So let's set main to dist/index.js.

Other than the entry point, we also need to specify the main types declaration file. In this case, that would be dist/index.d.ts.

We also need to specify which files to ship with the package. Of course, we need to ship our built JavaScript files, but since we are using sourceMap and declarationMap, we also need to ship src.

Here's a reference package.json with all of that:

{
"name": "the-greatest-sdk", // Your package name
"version": "1.0.3", // Your package version
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc"
},
"keywords": [], // Add related keywords
"author": "liblab", // Add yourself here
"license": "ISC",
"files": [
"dist",
"src"
],
"devDependencies": {
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
}
}

Publishing to NPM

Publishing to NPM is not difficult. I strongly recommend taking a look at the official instructions, but here are the general steps:

  1. Make sure your package.json is set up appropriately.
  2. Build the project (with npm run build if you followed the guide).
  3. If you haven't already, authenticate to npm with npm login (you'll need an npm account).
  4. Run npm publish.

Keep in mind that if you update your package, you'll need to increase the version option in your package.json before publishing again.

There are more sophisticated (and recommended) ways to go about publishing, like using GitHub actions and releases, especially for open-source packages, but that’s out of scope for this post.

Conclusion

By following the discussed approach your typescript npm packages will now provide better type-hinting, auto-completion and support ES Modules (ESM) and CommonJS (CJS) style imports, making them more accessible and usable by a wider audience.

Here at liblab, we know that preparing your project for NPM can be annoying. That's why our TypeScript SDKs come prepared with all the necessary adjustments for proper publishing to NPM. We'll even help you set up your CI/CD for seamless publishing. Contact us here to learn more about how we can help automate your API’s SDK creation.