Two options work as a pair. module says what import syntax your output uses. moduleResolution says how tsc finds the file behind "./util.js" or "dep/feature". TypeScript does not resolve modules on its own terms: the setting should mimic the tool that will actually run your code, or tsc accepts imports that fail at runtime (or rejects ones that work).
nodenext(alsonode16,node18,node20). Models Node.js. Each file is ESM or CommonJS depending on its extension (.mts/.cts) and the nearestpackage.json"type". In ESM files, relative imports need an explicit extension, and you write the output extension (./util.jsfindsutil.ts). Packages are entered through"exports", with theimportorrequirecondition chosen per file. Settingmodule: nodenextimpliesmoduleResolution: nodenext, and mismatches are errors (TS5109/TS5110).bundler. Models Vite, webpack, esbuild, Bun. Extensionless paths and directory indexes are allowed, and"exports"and"imports"are honoured. Pair it withmodule: preserve(oresnext), usually withnoEmit.node10andclassic. Legacy.node10ignores"exports". TS 6.0 deprecated both; TS 7.0 removed them.
Choose by who consumes the emitted JavaScript: Node directly gives nodenext; a bundler or runtime that reads TypeScript gives bundler. For libraries, the TypeScript docs advise nodenext-style settings: bundler accepts extensionless relative exports that then break in Node.
Resolution through "exports" matches the types condition first, so list types before default. A subpath not listed in "exports" cannot be imported, in tsc or in Node.