The exports map controls what consumers can import, and mistakes break bundlers, Node and TypeScript differently.
The exports field in package.json defines a package's public entry points. Once present, consumers can import only the listed paths; deep imports like pkg/dist/internal.js fail with ERR_PACKAGE_PATH_NOT_EXPORTED. That encapsulation lets you refactor internals freely.
"exports": {
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" },
"./utils": "./dist/utils.js",
"./package.json": "./package.json"
}Conditions are matched in object order, first match wins: types (TypeScript), import, require, node, browser, development/production, and default as the fallback. Put types first and default last.
Related fields:
mainandmoduleare the legacy entry points; keepmainonly for old tools.type: "module"makes.jsfiles ESM;.cjsand.mjsoverride per file.importsdefines private aliases starting with#, like"#db": "./src/db/index.js", usable only inside the package.sideEffects: falsetells bundlers unused modules can be dropped entirely.
Dual ESM/CommonJS packages risk the dual package hazard: two copies of module state loaded at once. Since Node 22 can require() ESM, publishing ESM-only is increasingly viable. Check your package with publint and @arethetypeswrong/cli.
Example
{
"name": "@ann/slugify",
"version": "2.0.0",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./locales/*": "./dist/locales/*.js",
"./package.json": "./package.json"
},
"imports": {
"#internal/*": "./src/internal/*.js"
},
"main": "./dist/index.cjs",
"sideEffects": false,
"files": ["dist"]
}
// import { slugify } from "@ann/slugify" OK
// import de from "@ann/slugify/locales/de" OK
// import x from "@ann/slugify/dist/index.js" ERR_PACKAGE_PATH_NOT_EXPORTEDRemember
exportsdefines the only importable paths- Conditions match in order: types first, default last
importsgives private#aliases inside the package- Validate packages with publint and arethetypeswrong
Watch out for
- Putting
typesafterimportin a condition object, so TypeScript never sees it.