TypeScript Library Packaging (2026)
The 2026 defaults
Public npm library (new project)
ESM-only · "type": "module" · exports map with "types" first tsdown or tsc for build · .d.ts + .d.ts.map for types moduleResolution: "nodenext" · target: "es2022" erasableSyntaxOnly: true · verbatimModuleSyntax: true Validate with publint + arethetypeswrong
Internal monorepo package
No build step · exports point to raw .ts source private: true · workspace:* protocol (pnpm) App's bundler handles transpilation
Quick decision tree
Module format
-
Ship ESM-only. require(esm) is stable in Node.js 22+. Dual CJS+ESM is unnecessary overhead.
-
Only dual-publish if you have known CJS consumers on Node.js < 22.
Build strategy
-
Single entry-point -> bundle with tsdown (or tsup)
-
Multi-export library -> file-to-file with tsc or unbuild mkdist
-
Internal monorepo package -> no build at all (raw .ts)
-
Never minify library code (let the app bundler do it)
Raw .ts on npm?
-
No. Node.js blocks .ts from node_modules/ . Transpile to JS + .d.ts .
-
Internal monorepo packages can use raw .ts (bundler handles it).
-
JSR accepts raw .ts directly.
moduleResolution
-
Libraries: nodenext
-
Apps: bundler
Minimal package.json
{ "name": "my-library", "version": "1.0.0", "type": "module", "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "files": ["dist"], "sideEffects": false }
Minimal tsconfig.json
{ "compilerOptions": { "strict": true, "module": "NodeNext", "moduleDetection": "force", "verbatimModuleSyntax": true, "erasableSyntaxOnly": true, "isolatedModules": true, "esModuleInterop": true, "resolveJsonModule": true, "target": "es2022", "lib": ["es2022"], "outDir": "dist", "rootDir": "src", "declaration": true, "declarationMap": true, "sourceMap": true, "skipLibCheck": true, }, "include": ["src"], }
Exports map rules
-
"types" condition MUST be first (most common mistake)
-
"default" condition should be last (fallback)
-
Once you add exports , unlisted deep imports throw ERR_PACKAGE_PATH_NOT_EXPORTED
-
.mjs output needs .d.mts types; .cjs needs .d.cts
See exports-map.md for patterns (subpaths, wildcards, dual CJS/ESM, monorepo).
Validation (always run before publishing)
npx publint # lint package.json npx @arethetypeswrong/cli my-pkg # validate type resolution npm pack --dry-run # preview published files
References
Topic File
Exports map patterns exports-map.md
tsconfig deep dive tsconfig.md
ESM vs CJS decisions esm-cjs.md
Build tools comparison build-tools.md
erasableSyntaxOnly & Node.js type stripping erasable-syntax.md
Monorepo setup monorepo.md
TypeScript 6.0 migration typescript-6.md
Registries (npm, JSR, vlt) registries.md
Import extensions & module resolution import-extensions.md
oxc transformation tools oxc-transforms.md
Key principles
-
ESM-only is the default. CJS compatibility via require(esm) is stable.
-
"types" first in exports. Always.
-
Ship .d.ts.map declaration maps. Enables "Go to Definition" into source.
-
Enable erasableSyntaxOnly . Aligns with Node.js type stripping and TC39 direction.
-
Use as const objects instead of enums. Better compatibility, same type safety.
-
Validate before publishing. publint + arethetypeswrong catch most issues.