Backend Development 25 min read

How to Publish a Fully Production‑Ready TypeScript Package to npm

This step‑by‑step guide shows how to create, configure, test, and publish a TypeScript npm package using Git, Prettier, tsup, Vitest, GitHub Actions CI, and Changesets for versioning and release automation.

Code Mala Tang
Code Mala Tang
Code Mala Tang
How to Publish a Fully Production‑Ready TypeScript Package to npm

In this guide we start from an empty directory and walk through publishing a fully production‑ready npm package, covering Git version control, TypeScript, Prettier, export checks, building with tsup, testing with Vitest, CI with GitHub Actions, and versioning with Changesets.

Use Git for version control

Write code with TypeScript for type safety

Format code with Prettier

Check exports with @arethetypeswrong/cli

Compile TypeScript to CJS and ESM with tsup

Run tests with Vitest

Run CI with GitHub Actions

Version and publish with Changesets

.1:Initialize Repository

Run the following command to create a new Git repository:

<code>git init</code>

1.2:Set .gitignore

Create a .gitignore file at the project root and add:

<code>node_modules</code>

1.3:Create Initial Commit

Run the following commands to make the first commit:

<code>git add .
git commit -m "Initial commit"</code>

1.4:Create a New Repository on GitHub

Using the GitHub CLI, create a new repository (example name tt-package-demo ):

<code>gh repo create tt-package-demo --source=. --public</code>

1.5:Push to GitHub

Push your code to GitHub:

<code>git push --set-upstream origin main</code>

Next we will create a package.json , add a license , a LICENSE file, and a README.md file.

2.1:Create package.json File

Create a package.json with the following content:

<code>{
  "name": "tt-package-demo",
  "version": "1.0.0",
  "description": "A demo package for Total TypeScript",
  "keywords": ["demo", "typescript"],
  "homepage": "https://github.com/yourgithub/tt-package-demo",
  "bugs": {"url": "https://github.com/yourgithub/tt-package-demo/issues"},
  "author": "Matt Pocock <[email protected]> (https://totaltypescript.com)",
  "repository": {"type": "git", "url": "git+https://github.com/yourgithub/tt-package-demo.git"},
  "files": ["dist"],
  "type": "module"
}
</code>

name is the npm package name (must be unique).

version follows semver, e.g., 0.0.1 .

description and keywords help with npm search.

homepage points to the repo or docs.

bugs URL for issue reporting.

author identifies you; you can add contributors if needed.

repository links to the GitHub repo.

files lists files to include when publishing (here dist ).

type set to module indicates ESM.

2.2:Add license Field

Add a MIT license field to package.json :

<code>{
  "license": "MIT"
}</code>

2.3:Create LICENSE File

Create a LICENSE file with the MIT license text (replace [year] and [fullname] with the current year and your name).

<code>MIT License

Copyright (c) [year] [fullname]

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

... (full license omitted for brevity) ...
</code>

2.4:Create README.md File

Create a README.md describing the package, e.g.:

<code>**tt-package-demo**

A demo package for Total TypeScript.
</code>

3.1:Install TypeScript

Install TypeScript as a dev dependency:

<code>npm install --save-dev typescript</code>

3.2:Set Up tsconfig.json

Create a tsconfig.json with these options (core options shown):

<code>{
  "compilerOptions": {
    "esModuleInterop": true,
    "skipLibCheck": true,
    "target": "es2022",
    "allowJs": true,
    "resolveJsonModule": true,
    "moduleDetection": "force",
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "module": "NodeNext",
    "outDir": "dist",
    "rootDir": "src",
    "sourceMap": true,
    "declaration": true,
    "declarationMap": true
  }
}
</code>

3.3:Configure DOM Types (optional)

If your code runs in the browser, skip this step. Otherwise add:

<code>{
  "compilerOptions": {
    "lib": ["es2022"]
  }
}
</code>

3.4:Create a Source File

Create src/utils.ts :

<code>export const add = (a: number, b: number) => a + b;
</code>

3.5:Create an Index File

Create src/index.ts :

<code>export { add } from "./utils.js";
</code>

3.6:Set build Script

Add a build script to package.json :

<code>{
  "scripts": {
    "build": "tsc"
  }
}
</code>

Running npm run build compiles the TypeScript to JavaScript in dist .

3.7:Run Build

<code>npm run build</code>

3.8:Add dist to .gitignore

<code>dist</code>

3.9:Set ci Script

<code>{
  "scripts": {
    "ci": "npm run build"
  }
}
</code>

4.1:Install Prettier

<code>npm install --save-dev prettier</code>

4.2:Create .prettierrc

<code>{
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 80,
  "tabWidth": 2
}
</code>

4.3:Add format Script

<code>{
  "scripts": {
    "format": "prettier --write ."
  }
}
</code>

4.4:Run format Script

<code>npm run format</code>

4.5:Add check-format Script

<code>{
  "scripts": {
    "check-format": "prettier --check ."
  }
}
</code>

4.6:Add check-format to CI

<code>{
  "scripts": {
    "ci": "npm run build && npm run check-format"
  }
}
</code>

5.1:Install @arethetypeswrong/cli

<code>npm install --save-dev @arethetypeswrong/cli</code>

5.2:Add check-exports Script

<code>{
  "scripts": {
    "check-exports": "attw --pack ."
  }
}
</code>

5.3:Run check-exports

<code>npm run check-exports</code>

Initially you will see resolution failures for Node and bundler.

5.4:Add main Field

<code>{
  "main": "dist/index.js"
}
</code>

5.5:Run check-exports Again

<code>npm run check-exports</code>

Now only a warning for Node 16 (CJS) remains.

5.6:Fix CJS Warning (optional)

If you do not want to support CJS, change the script to ignore the rule:

<code>{
  "scripts": {
    "check-exports": "attw --pack . --ignore-rules=cjs-resolves-to-esm"
  }
}
</code>

5.7:Add check-exports to CI

<code>{
  "scripts": {
    "ci": "npm run build && npm run check-format && npm run check-exports"
  }
}
</code>

6.1:Install tsup

<code>npm install --save-dev tsup</code>

6.2:Create tsup.config.ts

<code>import { defineConfig } from "tsup";

export default defineConfig({
  entryPoints: ["src/index.ts"],
  format: ["cjs", "esm"],
  dts: true,
  outDir: "dist",
  clean: true,
});
</code>

6.3:Change build Script to Use tsup

<code>{
  "scripts": {
    "build": "tsup"
  }
}
</code>

6.4:Add exports Field

<code>{
  "exports": {
    "./package.json": "./package.json",
    ".": {
      "import": "./dist/index.js",
      "default": "./dist/index.cjs"
    }
  }
}
</code>

6.5:Run check-exports Again

<code>npm run check-exports</code>

All checks should now be green.

6.6:Use tsup as Linter (add noEmit )

Add noEmit": true to compilerOptions in tsconfig.json to let TypeScript only type‑check.

6.6.2:Remove Unused Fields

Remove outDir , rootDir , sourceMap , declaration , and declarationMap from tsconfig.json .

6.6.3:Change module to Preserve

<code>{
  "compilerOptions": {
    "module": "Preserve"
  }
}
</code>

This allows imports without the .js extension.

6.6.4:Add lint Script

<code>{
  "scripts": {
    "lint": "tsc"
  }
}
</code>

6.6.5:Add lint to CI

<code>{
  "scripts": {
    "ci": "npm run build && npm run check-format && npm run check-exports && npm run lint"
  }
}
</code>

7.1:Install vitest

<code>npm install --save-dev vitest</code>

7.2:Create Test File

<code>import { add } from "./utils";
import { test, expect } from "vitest";

test("add", () => {
  expect(add(1, 2)).toBe(3);
});
</code>

7.3:Add test Script

<code>{
  "scripts": {
    "test": "vitest run"
  }
}
</code>

7.4:Run Tests

<code>npm run test</code>

7.5:Add dev Script for Watch Mode

<code>{
  "scripts": {
    "dev": "vitest"
  }
}
</code>

7.6:Add test to CI

<code>{
  "scripts": {
    "ci": "npm run build && npm run check-format && npm run check-exports && npm run lint && npm run test"
  }
}
</code>

8.1:Create GitHub Actions Workflow

<code>name: CI

on:
  pull_request:
  push:
    branches:
      - main

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Use Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
      - name: Install dependencies
        run: npm install
      - name: Run CI
        run: npm run ci
</code>

9.1:Install @changesets/cli

<code>npm install --save-dev @changesets/cli</code>

9.2:Initialize Changesets

<code>npx changeset init</code>

9.3:Make Changesets Public

Edit .changeset/config.json to set "access": "public" .

9.4:Enable Automatic Commit

Set "commit": true in .changeset/config.json .

9.5:Add Local Release Script

<code>{
  "scripts": {
    "local-release": "changeset version && changeset publish"
  }
}
</code>

9.6:Run CI Before Publishing

<code>{
  "scripts": {
    "prepublishOnly": "npm run ci"
  }
}
</code>

9.7:Add a Changeset

<code>npx changeset</code>

Mark the version as patch and describe it (e.g., "initial release").

9.8:Commit Changes

<code>git add .
git commit -m "Prepare for initial release"
</code>

9.9:Run Local Release Script

<code>npm run local-release</code>

This runs the CI, versions the package, and publishes it to npm.

9.10:View Package on npm

<code>http://npmjs.com/package/&lt;your-package-name&gt;</code>

You should now see your published package.

Summary

You now have a fully configured TypeScript package with Prettier, export checks, tsup compilation, Vitest testing, GitHub Actions CI, and Changesets for versioning and publishing.

TypeScriptcinpmprettierGitHub Actionspackagevitest
Code Mala Tang
Written by

Code Mala Tang

Read source code together, write articles together, and enjoy spicy hot pot together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.