Shipping

Expert · 20 min read · ▶ live playground · ✦ checkpoint

Shipping a typed library means proving the package people install contains runnable JavaScript and declaration files, the .d.ts files their editor reads for types. A green local build is not enough; you test the packed package.

A shipping loop is the repeatable path from source code to a releasable package. An artifact is the built file set you hand to users. For a typed library, the artifact is the sealed box users open, not the workbench where you wrote the code.

The promise from 24.2 now has to survive tools. Your exported types are still promises. Your .d.ts files are the paper copy of those promises.

Build JavaScript and declarations

A declaration file is a .d.ts file that describes a JavaScript API to TypeScript. A bundler is a build tool that turns source files into distributable JavaScript, often by combining files and rewriting module syntax.

Many libraries use tsup, a TypeScript library bundler wrapper, or esbuild, a fast tool that turns TypeScript or JavaScript source into JavaScript output, somewhere in this step. Keep the claim narrow: esbuild can transform TypeScript syntax into JavaScript, but it does not type-check and it does not emit declarations. If you use esbuild directly, pair it with tsc --noEmit for type checking and tsc --emitDeclarationOnly for declarations.

Shown as read-only reference code:

import { defineConfig } from "tsup";
 
export default defineConfig({
  entry: ["src/index.ts"],
  format: ["esm"],
  dts: true,
  sourcemap: true,
  clean: true,
});

That config is not a recommendation to install anything here. It is the shape you inspect in a real package that already uses tsup: one entry point, module output, source maps, files that map built JavaScript back to TypeScript source, and declaration output.

If your repo uses esbuild directly, the safer mental model is split work:

{
  "name": "launch-kit",
  "version": "1.2.0",
  "type": "module",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    }
  },
  "files": ["dist", "README.md", "LICENSE"],
  "scripts": {
    "typecheck": "tsc --noEmit",
    "build:js": "esbuild src/index.ts --bundle --format=esm --outfile=dist/index.js --sourcemap",
    "build:types": "tsc --emitDeclarationOnly --declaration --declarationMap --outDir dist",
    "build": "npm run typecheck && npm run build:js && npm run build:types"
  }
}

Notice the order. Type checking is explicit. JavaScript output is explicit. Declaration output is explicit. That makes a broken public promise easier to locate.

Pack the thing users install

A tarball is the .tgz archive npm creates for an installable package. A smoke test is a small check that proves the most important path works before deeper testing.

The mistake is testing src/ and assuming the package is fine. Consumers do not install your working tree. They install the packed box after files, exports, generated declarations, and build scripts have all had a chance to drop something.

Shown as a read-only terminal transcript:

# read-only package smoke test
npm run typecheck
npm run build
npm pack --dry-run
mkdir -p ../launch-kit-smoke ../launch-kit-consumer
npm pack --pack-destination ../launch-kit-smoke
cd ../launch-kit-consumer
npm init -y
npm install ../launch-kit-smoke/launch-kit-1.2.0.tgz
node -e "import('launch-kit').then((kit) => console.log(Object.keys(kit)))"
npx tsc --noEmit

npm pack --dry-run reports what would go into the tarball without writing the archive. npm pack creates the archive. Installing that archive into a tiny consumer project proves the public import path, runtime file, and declaration file line up.

Keep the smoke test small. Import the package the way docs tell users to import it. Run one realistic function. Run tsc --noEmit in the consumer fixture. If the package supports multiple entry points, smoke-test each public path.

Version with changesets

Changesets is a release tool that reads changeset files to update versions and changelogs, release-history files users read. A changeset is a small release note file that says which package changed, which version bump it needs, and why; a version bump is the major/minor/patch level for the next release. It turns the type-contract judgment from 18.2 into a file reviewers can read.

Shown as read-only reference text:

---
"launch-kit": minor
---
 
Add createLaunchPlan for typed launch drafts.

That file says launch-kit needs a minor release and gives the changelog sentence. Later, the Changesets version step can collect changeset files, update package versions, and update changelogs. The publish step belongs in your real release workflow, after build and artifact checks pass.

For type changes, use the same honesty you learned in 18.2:

  1. Removing an export is major.
  2. Narrowing a public parameter is major.
  3. Adding a new optional option is usually minor.
  4. Fixing docs or internal build code is usually patch.

The changeset is not magic. It is a reviewable note tied to semantic versioning, the major/minor/patch system for signaling compatibility. It makes release intent visible before the package leaves your repo.

Keep docs checked

A documentation generator is a tool that builds API docs from source comments and exported types. TypeDoc is a documentation generator that can generate documentation from TypeScript exports, so it pairs well with public API design.

Shown as read-only reference config:

{
  "entryPoints": ["src/index.ts"],
  "out": "docs/api",
  "excludeInternal": true
}

Docs drift when examples are copied by hand and never checked again. Twoslash is a TypeScript code-sample format that can let the compiler power hovers, errors, and type callouts in documentation.

Shown as read-only reference code:

import { createLaunchPlan } from "launch-kit";
 
const draft = createLaunchPlan("Search launch", "pro");
draft.karmaRequired;
//    ^?

The habit matters more than the brand. Public docs should be built from exported API, and code samples should be checked by tooling when your documentation build supports it. A stale example is another broken promise.

Put the loop together:

  1. Type-check source.
  2. Build JavaScript.
  3. Emit declarations.
  4. Pack the artifact.
  5. Install the packed artifact in a tiny consumer.
  6. Version with a changeset.
  7. Generate docs from the public surface.

Next, the course steps into type-level programming. The same release discipline still applies because clever public types are still public promises.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion