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:
- Make sure your
package.json
is set up appropriately. - Build the project (with
npm run build
if you followed the guide). - If you haven't already, authenticate to npm with
npm login
(you'll need an npm account). - 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.