Right, so you’ve built something genuinely useful—or at least, you’ve built something that compiles without throwing seven hundred type errors. Congratulations. Now comes the fun part: making other people’s computers run it. This isn’t just about slapping some files in a zip folder and calling it a day. We’re building a proper package, the kind you’d publish to npm or use across a dozen microservices without wanting to tear your hair out. Let’s get this done properly.

The Humble package.json: Your Manifest Destiny

Everything starts and ends with your package.json. It’s the ID card, the shipping manifest, and the list of ingredients for your package. The most important field for our TypeScript purposes? main and types.

{
  "name": "my-awesome-library",
  "version": "1.0.0",
  "description": "It does a thing, but with types!",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "prepublishOnly": "npm run build"
  },
  "devDependencies": {
    "typescript": "^5.0.0"
  },
  "files": [
    "dist/"
  ]
}

See that? main points to the compiled JavaScript entry point, and types points to its corresponding TypeScript declaration file. This is how Node.js knows what to run and how TypeScript knows what types your exported functions have when someone imports your package. If you forget the types field, your consumers will be left in a dark, type-less void, and they will rightfully complain.

The files array is your bouncer; it tells npm exactly which files to let into the club. Only what’s listed here gets published. This is why you npm pack locally first—to double-check the guest list before sending out the invitations.

Taming the TypeScript Compiler: A tsconfig.json for Publishing

Your local tsconfig.json is probably a mess of "strict": true and "noImplicitAny": true because you’re a responsible developer. But for building a distributable package, we need a specific, production-oriented config.

Create a tsconfig.prod.json that extends your base config. The key is to output declarations (.d.ts files) and target a sensible version of JavaScript.

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "declaration": true,        // Crucial: Generate .d.ts files
    "outDir": "dist",           // Send all output to a ./dist folder
    "noEmitOnError": true,      // Don't output JS if there's type errors. Obviously.
    "target": "ES2020",         // A sane modern target. Adjust based on your support needs.
    "sourceMap": true           // Optional, but very nice for your consumers when debugging.
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

Why ES2020? It’s a great balance between modern features and broad support. You could target ESNext, but then you’re offloading all the transpilation work to every single consumer of your package. Don’t be that person. Be the person who provides a clean, well-defined API surface.

The Build Script and the Holy prepublishOnly Hook

Your build script is just tsc -p tsconfig.prod.json. But the real pro move is hooking it into the npm lifecycle.

{
  "scripts": {
    "build": "tsc -p tsconfig.prod.json",
    "prepublishOnly": "npm run build"
  }
}

The prepublishOnly script runs automatically when you run npm publish. It’s a final, crucial safety net. It ensures that the code being published is freshly built from your source, every single time. This prevents the ultimate facepalm moment: publishing your raw src/ directory with half-baked // TODO comments instead of the compiled dist/ output.

What to Ignore (The .npmignore File)

You have a .gitignore to keep crap out of your repo. You need an .npmignore to keep crap out of your package. If you don’t provide one, npm will use your .gitignore by default, which is often wrong.

# .npmignore
src/
tsconfig.json
tsconfig.prod.json
.gitignore
.npmignore

# Unless you're a testing library, don't ship your tests
**/*.test.ts
**/__tests__/

# Definitely don't ship your secrets, even by accident
.env
.env.local

The rule of thumb: only the compiled output in dist/ and the package.json (and maybe a LICENSE and README.md) should be published. Nothing else. This makes your package lean, mean, and secure.

The Consumer’s Experience: It Just Works™

When you get this right, the experience for the person using your package is seamless. They run:

npm install my-awesome-library

Then in their code:

import { doTheThing } from 'my-awesome-library';

// They get full autocomplete and type checking here.
const result = doTheThing('input');

They never see your TypeScript source. They get the clean JavaScript you compiled, and the type definitions that tell their editor exactly how doTheThing works. This is the magic. You’ve successfully teleported your brilliant code and its intelligence into their project without any of the mess. That’s a win. Now go publish something.