Below is a practical, end-to-end guide to **`compilerOptions.customConditions`** in *tsconfig.json*—what it is, when to reach for it, and how to wire everything up so that your editor, the TypeScript compiler, your bundler, and Node itself all resolve the same files.
customConditions
lets you tell the TypeScript module resolver to recognise extra [conditional-export “conditions”] beyond the built-ins (import
, require
, node
, default
, etc.).
At compile-time, tsc
will use those additional keys whenever it looks at a package’s "exports"
or "imports"
map, exactly the same way Node or modern bundlers would at run-time.(typescriptlang.org)
Why bother? If a package (maybe your package) publishes environment-specific bundles—
"browser"
,"worker"
,"development"
,"production"
,"types"
, and so on—you want your editor/IDE hover info, jump-to-definition, and build output to follow the same branch that your runtime will use. That is precisely whatcustomConditions
unlocks.
Where it matters | Minimum version / flag | Notes |
---|---|---|
TypeScript | v5.0 – add "customConditions" |
Option is only honoured when moduleResolution is "node16" , "nodenext" or "bundler" .(typescriptlang.org) |
Node runtime | v14.9.0 – --conditions flag for user conditions |
Node already resolves built-ins. You only need --conditions=<name> for custom ones.(nodejs.org) |
Bundlers | Webpack 5, Vite, Rollup 3 | They expose resolve.conditionNames /resolve.conditions or similar; list the same strings you put in customConditions . |
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"target": "es2022",
"customConditions": ["development"] // adds to (import, require, node…)
}
}
Result: tsc
(and therefore your IDE) will resolve my-lib/dist/dev.mjs
. Swap "development"
for "production"
and the prod build is used instead.
Step | What to do | Why |
---|---|---|
1. Pick a resolver | Set "moduleResolution" to "node16" , "nodenext" or "bundler" . |
Classic resolution cannot look at "exports" /"imports" at all. |
2. List your extra conditions | "customConditions": ["browser", "development"] |
Order does not matter; they’re appended to Node’s default list. |
3. Mirror it in your bundler | Webpack: resolve.conditionNames: ["browser","development"] |
Keeps dev-server behaviour identical to tsc . |
4. (Optional) pass flags to Node | node --conditions=development app.js |
Needed only if the runtime should match a non-default condition. |
5. Always include a "default" branch in packages you publish |
Fallback guarantees third-party tools that ignore your condition still work.(nodejs.org) |
{
"compilerOptions": {
"moduleResolution": "node16",
"customConditions": ["browser"]
}
}
import "foo" in… |
File chosen (foo ’s exports) |
---|---|
Tests run with ts-node | "node" ➜ ./dist/node.cjs |
Front-end code (IDE view + Vite dev server) | "browser" ➜ ./dist/browser.mjs |
Many packages now publish a "types"
condition (first in the object) so build tools can grab *.d.ts
without pulling JS. Add it to customConditions
to silence “Cannot find module ‘…’ ” when authoring libraries with separate type bundles.
Because tsc
resolves the actual file path, roll-up-style bundlers can now tree-shake entire environment branches away. Mark debug-only helpers behind a "development"
condition and they vanish from the production bundle automatically.
Pitfall | Fix |
---|---|
“Option 'customConditions' can only be used when moduleResolution is node16/nodenext/bundler” | Switch the resolver or remove the flag. |
Your condition is ignored at runtime | Pass --conditions=<name> to Node (or set NODE_OPTIONS="--conditions=…" ), and configure bundler conditions. |
Third-party tooling (eslint, SonarQube ≤ v9.9) warns “unknown compiler option” | Upgrade those tools; they need TS ≥ 5 support. |
Using "browser" and "node" without "default" |
Some environments mis-detect themselves; always add a safe "default" branch.(nodejs.org) |
- Use short, environment-style names:
"browser"
,"deno"
,"edge-runtime"
,"production"
. - Document every custom condition in your README for consumers.
- Keep conditions mutually exclusive when they represent modes (e.g. “development” vs “production”).
- Prefer
moduleResolution:"bundler"
for front-end projects—it side-steps many Node-specific edge cases. - Test the runtime path with
node --conditions=<name> -p "import('my-lib').then(m=>console.log(m))"
.
// tsconfig.json
{
"compilerOptions": {
"target": "es2022",
"module": "esnext",
"moduleResolution": "bundler",
"customConditions": ["browser", "development"],
"esModuleInterop": true,
"strict": true
}
}
Add the same condition list to your bundler and (optionally) to Node’s CLI flags, and you’re set—TypeScript, your dev server, and production runtime will all agree on which files to load.
That’s it! With customConditions
, TypeScript finally speaks the same conditional-exports language as Node and modern bundlers, eliminating whole classes of “works in the editor, breaks in prod” bugs.
Below is a “cheat-sheet” of the condition names you’re most likely to see (or want to publish) in a `package.json { "exports": … }` or `imports` map, grouped by where the names come from and how broadly they’re supported.
These are always evaluated by Node (you don’t need to pass --conditions
to enable them) and therefore by TypeScript when you use moduleResolution:"node16"
, "nodenext"
or "bundler"
.
Condition | Typical purpose | Notes |
---|---|---|
import |
ES-module consumers (import … ) |
Highest priority when the current module graph is ESM. |
require |
CommonJS consumers (require() calls) |
Picked when the calling module is CJS. |
node |
The Node runtime as a whole | Good place for Node-specific polyfills. |
default |
Universal fallback | Always include this so unknown tools still work. |
module-sync |
Synchronous module system that honours import (used by ts-node etc.) |
Added in Node 20. |
node-addons |
Branch that loads native .node addons |
Skipped if Node is run with --no-addons . |
These aren’t hard-coded into Node, but the docs give them an “official” blessing, so tooling recognises them if you list them and, at runtime, you opt-in with node --conditions=<name>
(or your bundler’s equivalent).
Condition | When to use it | Guidance |
---|---|---|
types |
Point to .d.ts files (or a .ts build) |
Should be the first key in the object so TS picks it without touching JS. |
browser |
Any web-browser environment | Include a matching "default" or "node" branch to keep Node users happy. |
development |
Dev-only builds with extra errors, React debug helpers, etc. | Mutually exclusive with production . |
production |
Minified / dead-code-stripped output | Mutually exclusive with development . |
Modern tooling (Vite, Bun, Deno, Cloudflare Workers, etc.) relies on these strings so one package can publish pre-built bundles for every platform. The list lives in the Runtime Keys spec and is growing, but the most established ones today are:
Runtime | Canonical condition |
---|---|
Node (Edge/light servers, Lambda, etc.) | node (already built-in) |
Bun | bun |
Deno | deno |
Electron (desktop apps) | electron |
React Native (mobile) | react-native |
Edge-Light (Vercel’s edge runtime) | edge-light |
workerd / Cloudflare Workers | workerd |
Fastly Compute@Edge | fastly |
(See the full registry for many more such as edge-routine
, netlify
, lagon
, moddable
, wasmer
, etc.) (runtime-keys.proposal.wintercg.org)
Not (yet) standardised, but you’ll often bump into them:
Condition | Where it appears | Typical use-case |
---|---|---|
worker or serviceworker |
Rollup-plugins, Workbox builds | Bundle a web-worker-safe version. |
modern / es2019 / es2022 |
Some library builds (e.g. Preact) | Ship later-syntax bundles for evergreen browsers. |
debug |
Libraries like debug or pino |
Drop debugging helpers when not wanted. |
edge-runtime |
Older Vercel examples (superseded by edge-light ) |
Same goal as edge-light . |
- Start with the core set:
import
,require
,node
,default
. - Add
types
if you ship separate.d.ts
or.ts
files. - Pick one platform key per actual runtime you ship (e.g.
browser
,deno
,bun
). - Optionally split dev/prod if your dev build is materially different.
Always remember:
- Put the most specific condition first in each object; resolution stops at the first match.
- Keep mutually exclusive branches (
development
vsproduction
) parallel, not nested, to avoid subtle dead-code-elimination bugs. - Fallback to
"default"
unless you have a very good reason not to—that way unknown tools, or a future runtime, still see something that works.
With those conventions you’ll hit the sweet spot where editors, TypeScript, Node, Bun, Deno, and SaaS edge platforms all pick the right file without you juggling extra build steps.