Capítulo 34 de 36
TypeScript's module analysis is fundamentally about modeling a host (whatever system — Node.js, a bundler, a browser — actually loads the emitted code) accurately enough to (1) emit a valid module format, (2) guarantee the emitted imports will really resolve at runtime, and (3) assign correct types to imported names — and the whole system only makes sense once you stop thinking in terms of input .ts files and start thinking in terms of output files, because that's what the host actually consumes.
<script>-tag files all share one global scope and can clobber each other's names — a module system gives each file its own scope plus an explicit way to expose things (export) and consume them (import). ESM (native import/export, standardized 2015, in Node.js since v12) and CJS (require/module.exports, Node's original system, no scope isolation built into the language spec) are the two systems TypeScript's docs focus on, though it can emit several others..ts files directly. A second transpiler/formatter that just passes imports/exports through untouched is not a host TypeScript needs to model. TypeScript itself is never a host — it only tries to accurately model whatever the real host will do..ts files being run directly), TypeScript's whole module-analysis job is: emit the output format the host expects, make sure the imports in that output actually resolve, and assign correct types to what's imported — all three judged against the host's real rules, not against what the source file happens to look like.module compiler option decides emitted format — and matters even with noEmit, because it also controls how TypeScript detects each file's module kind for type-checking/resolution purposes, independent of whether JS is actually written to disk.node16/node18/nodenext): .mjs/.cjs (and their TS counterparts .mts/.cts) are unambiguous by extension; a plain .js/.ts file's format depends on the nearest ancestor package.json's "type" field ("module" → ESM, anything else/absent → CJS). TypeScript applies this exact algorithm to input files to predict what the output file's real runtime format will be — getting this right is what prevents emitting import/export syntax into a file Node.js will parse as CJS (syntax error) or require/module.exports into a file Node.js will treat as ESM (runtime crash, since CJS globals aren't injected into ESM scope).import/export syntax can still emit as CommonJS (require/exports) depending on module and the detected format — so looking at a file's source syntax alone doesn't tell you whether it's "really" ESM or CJS at runtime. This ambiguity is exactly why TypeScript 5.0 added verbatimModuleSyntax, which forces each file's written import/export syntax to already match the form it will emit as (ESM syntax only in files that will emit ESM, import x = require(...)/export = only in files that will emit CJS) — recommended for mixed-format Node.js projects, not recommended for CJS projects that might migrate to ESM later.require ESM synchronously at all, only via dynamic import(); ESM default-importing CJS always binds to exports as a whole). module: node16/node18/nodenext enforces Node's actual version-specific rules; every other module setting combined with esModuleInterop gives bundler-like interop instead.tsc — whatever string appears in from "..." is emitted verbatim regardless of module; only the surrounding import/export/require syntax gets transformed. This means module specifiers in source must already be written in a form valid for the eventual output/runtime — the reason Node16/nodenext-targeting ESM code has to write relative imports with an explicit .js extension even though the source files are .ts.import/export link, but says nothing about how a specifier string maps to an actual file; that's each host's own invention. moduleResolution tells TypeScript which host's algorithm to imitate: classic (legacy, RequireJS-era, scheduled for deprecation), node10 (models pre-v12 Node.js, an increasingly bad model of modern Node.js), node16/nodenext (Node.js's actual dual ESM/CJS resolution rules — locked to the matching module setting), and bundler (models the permissive, extension-optional-even-for-package.json-exports-consumers behavior common to modern bundlers and Bun; requires module: esnext)../math resolves directly to math.ts" — think "main.ts maps to output main.js, which resolves via the module specifier to output math.js, which maps back to input math.ts for type information." This model explains otherwise-confusing behavior, like a Node16-mode .mts file needing to import a sibling via "./math.mjs" even though no math.mjs file exists next to the .mts sources yet (it will, after compilation, possibly in a different outDir) — the specifier has to be valid for the output pairing, not the input one..d.ts) are the type half of that output pairing for already-compiled code: tsc --declaration on one .ts input always produces both a .js and a matching .d.ts, so wherever the compiler finds a declaration file, it assumes a corresponding JS file exists with the shape the declaration describes — and it stops looking further once it finds either a source .ts or a .d.ts, for performance. The declaration file's own extension implies the paired JS extension: .d.ts→.js, .d.mts→.mjs, .d.cts→.cjs, and (with allowArbitraryExtensions) .d.*.ts→.* for non-JS assets imported as modules. Hand-written/hand-copied declaration files (common on DefinitelyTyped) that don't follow this naming discipline can produce false-positive errors for consumers — tools like @arethetypeswrong/cli exist specifically to catch that mismatch before publishing.tsx running .ts directly), the usual "must write the output extension in imports" restriction becomes unhelpful — noEmit/emitDeclarationOnly plus allowImportingTsExtensions lets source files import each other with real .ts extensions instead. moduleResolution: bundler is the matching resolution mode — Node.js-inspired but without Node's strict ESM extension-search restrictions.moduleResolution: bundler (or the legacy node10) can silently produce output that only actually resolves under a bundler's permissive rules — e.g. export * from "./utils" (no extension) compiles cleanly under bundler/esnext but crashes at runtime under real Node.js ESM resolution, which requires the extension. Since a library doesn't control its consumer's host, targeting module: node18 (which implies the stricter node16 resolution) is the safer default — code that satisfies Node's stricter rules tends to also work in bundlers and other runtimes, while the reverse isn't guaranteed. This guidance only applies when tsc itself produces the shipped output; a library that bundles before publishing shifts that responsibility to its bundler instead.// src/main.mts, compiled with moduleResolution: node16, outDir: dist
import { add } from "./math.mjs"; // .mjs, even though the sibling *source* file is math.mts
dist/main.mjs → dist/math.mjs), which TypeScript then maps back to src/math.mts for type information, not a direct input-to-input reference.moduleResolution | Models | Notes |
|---|---|---|
classic | legacy RequireJS-era resolution | scheduled for deprecation, avoid |
node10 (formerly node) | pre-v12 Node.js | poor model of modern Node.js, avoid for new projects |
node16 / nodenext | real Node.js dual ESM/CJS resolution | must pair with matching module setting |
bundler | modern bundlers / Bun | requires module: esnext; permissive, extension-optional even with exports |
module matters for type-checking accuracy even under noEmit — it's not purely an emit-format switch.tsc output should target the strictest applicable settings (node18/node16 resolution), since permissive bundler-only output can crash for consumers running under stricter hosts like real Node.js ESM..d.ts file's own extension (.d.ts/.d.mts/.d.cts) is a promise about its paired JS file's extension and format — hand-maintained declaration files that break this pairing cause false-positive errors downstream.module Compiler Option: the concrete option values this chapter's theory motivates.paths, node_modules, package.json exports) that moduleResolution modes described here actually perform.