Authoring and Publishing TypeScript Libraries
Turn your TypeScript into a polished npm package. You'll emit .d.ts declarations with declaration: true , wire up main / types / exports , build dual ESM + CJS output with tsup or tsc , follow semantic versioning , and npm publish so consumers get IntelliSense for free.
Learn Authoring and Publishing TypeScript Libraries in our free TypeScript course — an interactive lesson with runnable examples, a practice exercise and a…
Part of the free TypeScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Publishing a library is like boxing up a product for sale . The compiled .js is the product itself; the .d.ts files are the instruction manual that lets buyers use it correctly without opening it up; package.json is the label telling the shop where everything is; and semantic versioning is the edition number that tells returning customers exactly what changed since last time.
1. Emit Declaration Files
The single most important setting for a library is "declaration": true — it makes tsc emit .d.ts files describing your public API. Those declarations are what give consumers type-safety and autocomplete. Read the real config here:
2. The package.json Contract
package.json tells consumers where everything lives: main (CommonJS entry), types (the .d.ts ), and the modern exports field that defines per-condition entry points for import vs require . The files array controls what actually ships. Read it here:
3. What Actually Ships
When you run npm publish , only the files allowed by the files array (plus a few always-included files like README.md ) go into the tarball. Crucially, your .d.ts files must be in there — that's how consumers get types. The box models the filtering:
4. Semantic Versioning
Every publish needs a version. SemVer is MAJOR.MINOR.PATCH : bump patch for a fix, minor for a backwards-compatible feature, and major for a breaking change (which resets minor and patch to 0). Following it lets consumers upgrade with confidence. The box shows the bumping rules:
5. Your Public API Surface
A library's public API is exactly what its entry file export s — anything not exported stays private. Because you ship the matching .d.ts , TypeScript consumers get autocomplete and checking on those exact names. The box models the import experience:
Your turn. Fill in the two blanks marked ___ to bump a version for a new feature, then run it.
No blanks this time — write isReadyToPublish(pkg) yourself, requiring name, version, types, and main, then run it against a complete and an incomplete package.
Practice quiz
Which tsconfig option makes tsc emit .d.ts declaration files?
- "strict": true
- "declaration": true
- "emitTypes": true
Answer: "declaration": true. Setting "declaration": true tells tsc to generate .d.ts files alongside the compiled JavaScript.
What does the "types" field in package.json point to?
- The entry .d.ts declaration file for consumers
- The license
- The test folder
- The README
Answer: The entry .d.ts declaration file for consumers. "types" (or "typings") points to the package's main .d.ts so editors give consumers IntelliSense.
What is the purpose of shipping .d.ts files with your package?
- To replace the JavaScript
- To make the package smaller
- So consumers get type checking and autocomplete without your source
- To run the tests
Answer: So consumers get type checking and autocomplete without your source. The .d.ts files describe your public API, so TypeScript users get IntelliSense and type-safety from the compiled package.
What does the "main" field in package.json specify?
- The test command
- The CommonJS entry point that gets loaded by require()
- The author's name
- The git repository
Answer: The CommonJS entry point that gets loaded by require(). "main" is the entry module Node loads via require(), usually the compiled CommonJS build.
What does the modern "exports" field add over "main"?
- It runs the build
- It publishes the package
- Nothing, it is identical
- It lets you define separate entry points per condition, like import vs require
Answer: It lets you define separate entry points per condition, like import vs require. "exports" supports conditional entry points (import, require, types) and controls exactly what is importable.
In semantic versioning, which part increments for a BREAKING change?
- The major number
- The build number
- The patch number
- The minor number
Answer: The major number. SemVer is MAJOR.MINOR.PATCH; a breaking change bumps MAJOR, new compatible features bump MINOR, and fixes bump PATCH.
Which command publishes your package to the npm registry?
- npm release
- npm publish
- npm deploy
- npm build
Answer: npm publish. npm publish uploads the package (respecting files/.npmignore) to the registry for others to install.
What is a common tool for building dual ESM + CJS output with types?
- eslint
- vitest
- nodemon
- tsup
Answer: tsup. tsup (built on esbuild) bundles TypeScript into ESM and CJS and can emit .d.ts files in one step; plain tsc also works.
Why ship BOTH ESM and CommonJS builds?
- CJS is faster to download
- To double the file count for fun
- So both modern import users and older require() users can consume the package
- ESM is required by npm
Answer: So both modern import users and older require() users can consume the package. Dual output means consumers on either module system can use your library without friction.
Where do you control which files are actually included in the published tarball?
- The "scripts" field
- The "files" array in package.json (or .npmignore)
- The tsconfig include
- The README
Answer: The "files" array in package.json (or .npmignore). The "files" array whitelists what gets published (typically dist), keeping source and tests out of the tarball.