Capítulo 36 de 36
One tsconfig.json models exactly one runtime environment — pick module settings based on who the actual consumer of the output is (a bundler, Node.js directly, a browser with no build step, or unknown downstream consumers of a published library), never by habit or copy-paste.
tsconfig.json (linked via project references) rather than trying to satisfy every environment's globals and module behavior from one config.module: esnext + moduleResolution: bundler + esModuleInterop: true, typically with noEmit/emitDeclarationOnly (the bundler does the real transpiling), allowImportingTsExtensions, and verbatimModuleSyntax/isolatedModules for safety with single-file-transpiling tools. Avoid "type": "module" in package.json or .mts files in bundler-only projects for now — some bundlers vary their ESM/CJS interop behavior specifically in that configuration in ways moduleResolution: bundler can't currently model.module: nodenext is the one required setting — it implies moduleResolution: nodenext, esModuleInterop: true, and target: esnext automatically. Remember to actually set "type": "module" in package.json (or use .mts files) if ESM output is intended — nodenext supports emitting either format per-file, it doesn't force ESM by itself.ts-node: aims for compatibility with the same settings as "compile and run in Node.js directly" — treat it the same way.tsx: behaves more like a bundler than ts-node (permits extensionless/index specifiers, freely mixes ESM/CJS) — use the bundler settings cluster for it, not the Node.js cluster.module: nodenext (for its stricter, extension-enforcing resolution) combined with paths can approximate it — mapping bare/URL specifiers to local .d.ts files for type information, optionally paired with a real browser <script type="importmap"> so the runtime resolution (URL/import-map based) and TypeScript's type resolution (via paths) point at compatible targets even though they work completely differently.module: node18 (implying moduleResolution: node16) — Node.js-compatible code tends to also work in bundlers; the reverse isn't guaranteed (a bundler-only extensionless export * from "./utils" compiles cleanly under moduleResolution: bundler but crashes under real Node.js ESM resolution, which requires the explicit extension).target: the lowest ECMAScript version actually supported — controls both which syntax gets downleveled and (via the implied lib) which globals the compiler assumes are available, preventing accidental use of runtime features the library's stated minimum doesn't actually have.strict: true — code that only type-errors when strict is disabled is rare, but code that type-checks fine unstrict and then breaks for a strict-enabled consumer (e.g. a widening extends relationship only invalid under strictNullChecks) is a real, easy-to-hit trap for library authors who don't compile strict themselves.verbatimModuleSyntax: true — protects against two consumer-facing pitfalls: (1) import statements whose meaning is ambiguous depending on the consumer's esModuleInterop/allowSyntheticDefaultImports settings (neither value of those flags alone guarantees portability — only writing import syntax unambiguous regardless of them does), and (2) accidentally using export default in a module that will emit as CommonJS, which forces bundler users and Node ESM users to consume the module two different ways.declaration: true — without emitted .d.ts files, consumers get no type information at all.sourceMap/declarationMap: true (optional trade-off) — lets consumers debug into and "go to definition" through to the library's real TypeScript source, at the cost of shipping the maps (and, for declaration maps to be useful, the source files themselves).rootDir/outDir — not just good practice but necessary if the package also ships its .ts sources: without it, extension substitution (TypeScript's own "prefer .ts over .js for the same path" rule) would make consumers' compilers load the library's raw .ts source instead of its .d.ts declarations, causing type errors and needless re-checking.module: esnext + moduleResolution: bundler becomes acceptable, but with two caveats:
moduleResolution setting that's simultaneously correct for both halves; either bundler (may leave unsafe externalized imports unchecked) or nodenext (may over-restrict imports the bundler would actually handle fine) is a compromise, not a perfect model..d.ts) can produce an invalid, extension-missing specifier for Node.js consumers, silently degrading affected imports to any.@arethetypeswrong/cli) against all published output bundles before release is the practical mitigation.// Node.js app/library, compiling and running (or publishing) real output:
{ "compilerOptions": { "module": "nodenext", "verbatimModuleSyntax": true } }
// implies moduleResolution: nodenext, esModuleInterop: true, target: esnext
// Bundler-consumed app (webpack/esbuild/Vite):
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"noEmit": true,
"verbatimModuleSyntax": true
}
}
module/moduleResolution and emit strategy while sharing the same interop/safety flags.| Scenario | module | moduleResolution | Key extras |
|---|---|---|---|
| Bundler-consumed app | esnext | bundler | noEmit, esModuleInterop, verbatimModuleSyntax |
| Compiling/running in Node.js | nodenext | (implied) nodenext | set package.json "type" deliberately |
ts-node | same as "running in Node.js" | — | — |
tsx | same as "bundler-consumed app" | — | — |
| Browser ESM, no bundler | nodenext | (implied) nodenext | paths mapping URLs/bare specifiers to local .d.ts |
Published library (via tsc) | node18 | (implied) node16 | strict, verbatimModuleSyntax, declaration, split rootDir/outDir |
| Published library (bundled) | esnext | bundler | ensure declaration files are bundled/consistent too |
module/moduleResolution based on who actually consumes the output — never copy a config from an unrelated project type.node18/node16 resolution, strict: true), since code correct under strict settings tends to stay correct for more permissive consumers, but not vice versa.outDir from its rootDir — otherwise TypeScript's own extension-substitution rule can make consumers load raw .ts source instead of .d.ts declarations.moduleResolution values referenced here.esModuleInterop/verbatimModuleSyntax background this guide's recommendations build on.