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.
Search across all documentation pages
"type" & exportspackage.json fields "type" and "exports" define how Node resolves your package - they replace ambiguous main fields and lock down internal files from deep imports.
{
"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:
import '@acme/pkg/src/internal/foo.js'exports (TypeScript 5.6+, webpack 5, Node 24){
"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./users) publish focused entry points without barrel bloat"imports" field maps internal aliases (#internal/*) for package-private shortcuts"./package.json" export when tools need to read metadata"type": "module" - .js files are ESM; use .cjs for intentional CommonJS."exports" - object keys are public subpaths; values are target files or conditional objects."import", "require", "node", "default" pick variants per resolver.ERR_PACKAGE_PATH_NOT_EXPORTED.| Key | Meaning |
|---|---|
"." | Package root import |
"./feature" | Subpath feature entry |
"./package.json" | Explicit metadata export |
"#alias" | Internal import map (imports field) |
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
}
}Pair "types" condition (or typesVersions) so consumers get typings matching the export.
main without exports - modern tools may still allow deep imports. Fix: add "exports" to encapsulate..js in export targets - must point to files that exist after build. Fix: CI check that dist matches exports.import and require instances break instanceof. Fix: prefer ESM-only for app packages; document for libraries."./src/*" exposes internals. Fix: export only dist artifacts.exports point to compiled dist in published packages.| 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 |
Treats .js files as ESM. Use .cjs for CommonJS files in the same package.
Discouraged for libraries - export compiled JS plus .d.ts for stable consumer resolution.
Consumer imported a path not listed in exports - intentional encapsulation working.
Resolver matches conditions in order - import vs require vs default per Node algorithm.
Package-private import map for #aliases - not for external consumers.
Yes - "./features/*": "./dist/features/*.js" maps many entries with one pattern.
Test runners must respect exports - may need moduleNameMapper or default import conditions.
Optional for private apps, recommended for monorepo packages consumed by sibling services.
Provide both import and require conditions pointing to .js and .cjs builds - test both paths.
files array still controls tarball contents - exports controls runtime importability.
Start with ".": "./dist/index.js" equivalent to old main, then remove deep import paths consumers used.
Top-level "types" points to root typings; per-export "types" condition is more precise for subpaths.
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.
Reviewed by Chris St. John·Last updated Jul 16, 2026