How to Use Conditional Exports in package.json
The exports field in package.json allows
JavaScript package authors to define explicit entry points and control
how consumers access module files based on specific runtime
environments, module formats, or conditions. By replacing or augmenting
the legacy main field, conditional exports enable modern
packages to serve distinct builds—such as ECMAScript Modules (ESM),
CommonJS (CJS), TypeScript definitions, or browser-specific bundles—from
a single package configuration cleanly and securely.
The Basic Structure of Conditional Exports
In its simplest form, the exports field maps module
conditions directly to file paths. Instead of pointing to a single file,
you provide an object where the keys represent conditions and the values
represent the corresponding file paths.
{
"name": "my-library",
"exports": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"default": "./dist/index.cjs"
}
}In this configuration: - An ESM import statement loads
./dist/index.mjs. - A CommonJS require() call
loads ./dist/index.cjs. - Any other consuming environment
falls back to default.
Common Conditional Export Keys
Node.js and module bundlers recognize a standard set of built-in condition keys:
import: Matches when the package is loaded via standard ESM syntax (import 'my-library'orimport()).require: Matches when the package is loaded via CommonJS (require('my-library')).node: Matches any Node.js environment, supporting both ESM and CommonJS.browser: Matches client-side JavaScript environments and bundlers such as Vite, Webpack, or Rollup targeting the browser.types: Used by TypeScript to locate declaration files (.d.ts).development/production: Used by runtimes or bundlers to switch between debug/development builds and minified production builds.default: A generic fallback condition that matches if no other specific condition applies.
Conditional Subpath Exports
Conditional exports are not limited to the root entry point. You can
expose distinct subpaths (such as my-library/utils) with
their own set of conditions:
{
"name": "my-library",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./utils": {
"types": "./dist/utils.d.ts",
"browser": "./dist/utils.browser.js",
"node": "./dist/utils.node.js",
"default": "./dist/utils.js"
}
}
}This ensures consumers can import specific utilities
(import { formatDate } from 'my-library/utils') while
receiving the optimized build for their targeted runtime.
Nested Conditions
Conditions can be nested to handle complex runtime matrix requirements, such as differentiating between ESM and CJS within a specific platform:
{
"exports": {
"node": {
"import": "./dist/node.esm.js",
"require": "./dist/node.cjs.js"
},
"default": "./dist/universal.js"
}
}Condition Ordering and Evaluation Rules
Order matters significantly within the exports object.
Tools evaluate conditional keys from top to bottom and stop at the first
matching key:
- Place specific keys first: General keys like
defaultmust always be placed last. - TypeScript typing placement: The
typescondition must be listed beforeimportorrequireso TypeScript can resolve types before the runtime entry files. - Encapsulation: Any file or directory inside your
package not explicitly defined in the
exportsfield is private and cannot be imported by consumers, preventing accidental access to internal APIs.