package.json "type" & exports
package.json fields "type" and "exports" define how Node resolves your package - they replace ambiguous main fields and lock down internal files from deep imports.
Recipe
{
"name": "@acme/billing-sdk",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./package.json": "./package.json"
},
"files": ["dist"]
}When to reach for this:
- Publishing internal monorepo packages to npm or Verdaccio
- Blocking consumers from
import '@acme/pkg/src/internal/foo.js' - Dual CJS/ESM libraries during ecosystem transition
- Tooling that respects
exports(TypeScript 5.6+, webpack 5, Node 24)
Working Example
{
"name": "@acme/shared-types",
"type": "module",
"exports": {
".": "./dist/index.js",
"./users": "./dist/users.js",
"./package.json": "./package.json"
},
"imports": {
"#internal/*": "./src/internal/*.js"
}
}// consumer.ts
import { UserDto } from '@acme/shared-types/users';// inside @acme/shared-types package only
import { helper } from '#internal/helper.js';What this demonstrates:
"exports"whitelist is the only importable surface - deep paths fail without a match- Subpath exports (
./users) publish focused entry points without barrel bloat "imports"field maps internal aliases (#internal/*) for package-private shortcuts- Explicit
"./package.json"export when tools need to read metadata
Deep Dive
How It Works
"type": "module"-.jsfiles are ESM; use.cjsfor intentional CommonJS."exports"- object keys are public subpaths; values are target files or conditional objects.- Conditions -
"import","require","node","default"pick variants per resolver. - No match, no access - importing unexported paths throws
ERR_PACKAGE_PATH_NOT_EXPORTED.
Common export shapes
| Key | Meaning |
|---|---|
"." | Package root import |
"./feature" | Subpath feature entry |
"./package.json" | Explicit metadata export |
"#alias" | Internal import map (imports field) |
TypeScript Notes
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
}
}Pair "types" condition (or typesVersions) so consumers get typings matching the export.
Gotchas
- Only setting
mainwithoutexports- modern tools may still allow deep imports. Fix: add"exports"to encapsulate. - Forgetting
.jsin export targets - must point to files that exist after build. Fix: CI check thatdistmatchesexports. - Dual package hazard - separate
importandrequireinstances breakinstanceof. Fix: prefer ESM-only for app packages; document for libraries. - Wildcard exports too broad -
"./src/*"exposes internals. Fix: export onlydistartifacts. - Monorepo workspace protocol without building - consumers resolve to TS source incorrectly. Fix:
exportspoint to compileddistin published packages.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
main + module fields only | Unmaintained legacy libs | New packages in 2026 |
| TypeScript path aliases only | App-internal shortcuts | Published npm packages |
publishConfig.exports | npm-specific override | Single registry publish |
| No public API (app not lib) | Private deployable service | Shared monorepo package |
FAQs
What does type module do?
Treats .js files as ESM. Use .cjs for CommonJS files in the same package.
Can I export TypeScript source?
Discouraged for libraries - export compiled JS plus .d.ts for stable consumer resolution.
What is ERR_PACKAGE_PATH_NOT_EXPORTED?
Consumer imported a path not listed in exports - intentional encapsulation working.
How do conditional exports pick?
Resolver matches conditions in order - import vs require vs default per Node algorithm.
What is the imports field?
Package-private import map for #aliases - not for external consumers.
Do subpath patterns work?
Yes - "./features/*": "./dist/features/*.js" maps many entries with one pattern.
How does this affect Jest/Vitest?
Test runners must respect exports - may need moduleNameMapper or default import conditions.
Should apps use exports?
Optional for private apps, recommended for monorepo packages consumed by sibling services.
What about dual ESM/CJS packages?
Provide both import and require conditions pointing to .js and .cjs builds - test both paths.
Does npm pack respect exports?
files array still controls tarball contents - exports controls runtime importability.
How do I migrate from main to exports?
Start with ".": "./dist/index.js" equivalent to old main, then remove deep import paths consumers used.
What role does types field play?
Top-level "types" points to root typings; per-export "types" condition is more precise for subpaths.
Related
- ES Modules (import) - ESM syntax consuming exports
- Module Resolution Algorithm - resolution order
- CJS ↔ ESM Interop - dual condition exports
- Sharing Types with Frontend - monorepo packages
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.