Share and publish package APIs

Create a TypeScript npm Package That Publishes JavaScript and Declarations

A small TypeScript npm package template with a build configuration, package entry points, an archive check, and a separate consumer test.

Editorial illustration for Build an ESM TypeScript npm Package with JavaScript, Declarations, and an Export Map

A library build is ready to publish only when an intended consumer can use the package as packaged—not merely when the source project accepts the code. For a first template, make one narrow promise: importing the package root provides a CommonJS JavaScript entry point and a declaration entry point. Supporting ESM as well would be a second build and packaging decision, not something this configuration does automatically.

Choose the import you intend to support

This example exposes one function, sum, from src/index.ts. Consumers should import example-sum-kit, not reach into its src or dist directories. That leaves the package root as the public path while allowing the internal layout to change.

The project is deliberately small:

example-sum-kit/
├── src/
│   └── index.ts
├── tsconfig.json
├── package.json
└── dist/                 # generated by the build
    ├── index.js
    └── index.d.ts

Put this in src/index.ts:

export function sum(...values: number[]): number {
  return values.reduce((total, value) => total + value, 0);
}

The exported function has a JavaScript implementation to execute and a typed signature to publish. Its parameter and return types describe calls checked by TypeScript; they do not validate values passed by JavaScript at runtime.

Match the build plan to the package plan

Use tsconfig.json to compile only the production source. Tests and fixtures can live elsewhere in the repository without becoming inputs to this build.

{
  "compilerOptions": {
    "module": "CommonJS",
    "target": "ES2022",
    "rootDir": "./src",
    "outDir": "./dist",
    "declaration": true,
    "noEmitOnError": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["**/*.test.*", "**/*.fixture.*"]
}

outDir places emitted files under dist; with this single source entry point, the intended outputs are dist/index.js and dist/index.d.ts. Declarations are generated alongside JavaScript rather than maintained as separate handwritten files. noEmitOnError prevents an erroneous type check from producing a new build. The TypeScript TSConfig reference describes the output-directory behavior, while its compiler options reference documents noEmitOnError.

declarationMap is optional for this small package and is not enabled here. Do not confuse it with sourceMap: the latter produces maps for emitted JavaScript. If you add declaration maps, inspect the extra generated files and decide whether their source references belong in the published archive. The Node.js TypeScript publishing guide shows a configuration with declarations and declaration maps, but its NodeNext module setting is not the CommonJS-only choice made here.

Now make the package metadata name the built files, not src/index.ts:

{
  "name": "example-sum-kit",
  "version": "0.1.0",
  "type": "commonjs",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "require": "./dist/index.js"
    }
  },
  "files": ["dist"],
  "scripts": {
    "build": "tsc --project tsconfig.json"
  },
  "devDependencies": {
    "typescript": "^5.7.2"
  }
}

This is a CommonJS package: the root export advertises require, not a separate ESM import build. The files list opts into shipping dist, so an omitted build artifact can break the package even when that artifact exists locally. The Node.js publishing guide calls out that risk for opt-in package filtering.

Check the artifact a consumer receives

From the package directory, run these steps rather than assuming the paths in the manifest are correct:

npm install
npm run build
ls dist
cat dist/index.d.ts
npm pack --dry-run
npm pack

First compare dist with the two paths advertised by main, types, and exports. Emission establishes that the build wrote JavaScript and declarations; it does not establish that another project can resolve the package. Inspect the pack listing too. The archive must include both advertised files. If you want to examine the resulting archive directly on a system with tar, use:

tar -tf example-sum-kit-0.1.0.tgz

Then leave the library directory and create a separate consumer. These commands assume the consumer is a sibling of example-sum-kit:

cd ..
mkdir sum-consumer
cd sum-consumer
npm init -y
npm install ../example-sum-kit/example-sum-kit-0.1.0.tgz
npm install --save-dev [email protected]

Create check.js to exercise the runtime entry point:

const { sum } = require("example-sum-kit");
console.log(sum(2, 3));

Create check.ts to exercise declaration discovery and the return type:

import { sum } from "example-sum-kit";

const total: number = sum(2, 3);

For that TypeScript check, use this consumer tsconfig.json:

{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "target": "ES2022",
    "noEmit": true
  },
  "include": ["check.ts"]
}

Run the checks separately:

node check.js
npx tsc --project tsconfig.json

Executing check.js checks the demonstrated JavaScript import and call. Checking check.ts asks TypeScript to resolve the package and accept a typed use of its export; NodeNext resolution uses package exports. Neither result proves the function’s behavior for every input or provides runtime validation.

Before release, keep source exports intentional, rebuild after changing the public API, compare metadata paths with actual output, and inspect the archive again. If you need the earlier dependency-installation and strict-checking walkthrough, see Initialize a TypeScript npm Project and Verify Strict Checking; the additional job here is checking what a library consumer receives.

Find a note

Search by topic, title, or keyword.