How to make your own npm package with typescript
There is a lot of mystery around making your own
npm package. Every package
likely does it a bit differently, and it can be tricky to get a setup you like.
Should you use a "starter kit" or a boilerplate example? Or just roll your own?
Should you use a bundler? How do you use typescript? Well, why don't we try
starting from scratch and seeing where we can get?
TLDR: here is a github repo with a template package https://github.com/cmdcolin/npm-package-tutorial/
npm package can be very bare bones. In some sense, npmjs.com is just an
arbitrary file host, and you can upload pretty much anything you want to it.
The magic is in the package.json file, which tells npm:
- what files are part of your package (by default, the whole folder with your package.json is published, with the "files" field you can say what particular folders or files are uploaded or you can use .npmignore to choose which files NOT to publish)
- what to use as the "entry point" (e.g. the file that should be referenced when
const lib = require('mypackage'), this is governed by the "main" field, "module" field which is bundler specific, and "export maps" which are newer but tricky)
- what pre- and post- processing steps should be done when the package is being published (the various "scripts", where you can have "clean", "build", "test", "lint", "prebuild", etc)
- and more!
Let's try an experiment...
#Part 1: the most basic package with plain JS code in commonjs format
Open up a terminal, and run
This init command outputs something like this, and we accept the defaults
Then, you can create a file named
index.js (in your package.json it says
"main": "index.js" to refer to this file, the entrypoint)
index.js file, generally, you would do things like export a function
or functions. I will use commonjs exports here for maximum compatibility:
#Publishing a package
This npm package,
mypackage can now be published to
npm with a simple
This will prompt you for your npmjs.com username, password, email, and if needed, 2FA token (highly recommended)
#Using your package after it is published
Once it is published, you can use it in your create-react-app app or other npm package.
Then you can use
in any of your other codebases
#Summary of the simplest NPM package
This all seems pretty boring thus far but it tells us a couple things
- packages can be very very bare bones
- no transpiler or bundler is needed for publishing an npm package
- our package can consist of a single file and it is uploaded to npm, and the "main" field in package.json provides an entry point
- the filename index.js is not special, probably it is a hangover from the name index.html. you can use whatever name you want
#Part 2: Adding typescript
Let's try adding typescript
To do this, we will use the typescript compiler to compile a directory of files in our "src" directory and output the compiled files to a directory named "dist"
To start, let's add typescript
Our package.json now will have
typescript in it's
means that when someone installs your package, it they don't get typescript as a
dependency, it is just a dependency for while you are developing the library
Then we need to create a tsconfig.json for typescript to use
This will generate a
tsconfig.json file (needed by
typescript) with a bunch
of options, but I have stripped it down in my projects to look like this
Now, let's wrote a little typescript. We can now use "ESM" style code, we will compile it to commonjs format.
And then we will add a
"build" script to
package.json to compile the
library, and refer to the
"dist" directory for the
We can now run
And this will run the
"build" script we created, which in turn, just runs
tsc with no arguments.
You can also add a
"prebuild" script that clears out the old contents. In
fact, npm scripts generalizes the naming system -- you can make scripts with
whatever name you want, e.g.
To make this useful, we will use
rimraf (a node package) to make a
cross-platform removal of the
and then update your package.json
We could make it say "rm -rf dist" instead of "rimraf dist" (e.g. run arbitrary shell commands), but rimraf allows it to be cross-platform
#Making sure you create a fresh build before you publish
Without extra instructions, your
yarn publish command would not create a fresh
build and you could publish an older version that was lingering in the
We can use a
preversion script that will automatically get invoked when you
yarn publish to make sure you get a fresh build in the
before you publish
#Making sure you push your tag to github after publish
When you run
yarn publish, npm will automatically create a commit with the
version name and a git tag, it will not automatically push tag to your
postversion script that pushes the tag to your repo after your publish
We can use this to do incremental/watch builds
#Adding testing with ts-jest
You can use ts-jest to test your code. This involves installing jest, typescript, ts-jest, @types/jest, and then initializing a jest.config.json
We can then create a test
Then we can then create a script in the package.json that says
and then we can say
You can also create an alternative system where you use
various babel strategies to test your code, but if you are using
typescript works great.
#Add a .gitignore
Create a .gitignore with just a line that references this
dist folder and
#The future of ESM modules
There is a shift happening where modules are changing to be pure ESM rather than keeping commonjs equivalents
There are many challenges here, but one shortcut that I have used is to actually go "halfway to ESM" and just publish a "dual" package: one "main" field in the package.json referring to a commonjs file, and one "module" field with an ESM style build for bundlers. I do it like this:
The "module" field is understood by bundlers like webpack and you can do
slightly less polyfilling/babeling on it (hence the different
This is not "pure ESM" with the "type":"module" in package.json, but it does help to have less "babelification" (which in our case is done by tsc) of your source code.
This tutorial shows you how you can create a basic package that you can publish
npm. This little boilerplate includes these features:
- Makes clean build when running
- Pushes to github after publish
- Uses esm modules
You also have full control, and understand the decisions we took to get to this
point. This package does not use any bundling (rollup or webpack or otherwise).
It just uses
tsc is used to compile the files to the
dist folder, and the
dist folder is published to
If you need your package to be usable by consumers that don't themselves use
bundlers, consider looking into
<script type="module"> for importing ESM
modules in the browser, or you can bundle your library using rollup or webpack
and output e.g. a UMD bundle
This is a setup that works for me, but there are many ways to publish a package so take it with a grain of salt!
Also see my follow up rant: you may not need a bundler https://cmdcolin.github.io/posts/2022-05-27-youmaynotneedabundler
#Footnote 1 - what about monorepos?
There are many high powered "monorepo" setups like lerna, nx, turborepo, etc.
I think for many purposes, these can be a bit overkill. I would start with yarn workspaces. Basically, the way this works is you can have e.g. in your root package.json in your repo something likely
And then in your
lib directory you can have your library as we created above
app for example can be an instance of a
vite app that uses your library.
You can reference your lib by name in the
app folders package.json, and it
will automatically get the latest version of it that you have built from the
lib directory e.g. your
app will look at the libs "dist" folder: it's
compiled outputs. That means you can run
yarn tsc --watch in the lib folder to
continuously build it, and then e.g. when you are running e.g.
yarn dev in the
app directory, it will see updates to the
lib dist directory and auto-update
via hot module refresh
High powered solutions like nx, turborepo, etc may have solutions for
'automatically building all the stuff' without you explicitly having to run the
build in the
lib directory, but for simple monorepo setups, this works ok