Capítulo 33 de 36
moduleResolution controls how TypeScript turns an import string into a file on disk — it should match whatever algorithm the actual runtime or bundler uses, and shares several building blocks (extension substitution, node_modules lookup, package.json field support) across its different mode values.
mod.js tries mod.ts → mod.tsx → mod.d.ts → (falls back to) mod.js/mod.jsx; mod.mjs/mod.cjs similarly prefer mod.mts/mod.d.mts and mod.cts/mod.d.cts respectively. This happens regardless of the extension actually written in the import — import x from "./mod.js" can resolve to a real mod.ts file even though the specifier says .js.moduleResolution mode.import {} from "./a", no extension) only resolve when the target runtime/bundler context supports that omission — Node.js's own import resolution does not support it (though some bundlers do); TypeScript models this per-mode rather than universally allowing it. .mjs/.mts/.cjs/.cts extensions specifically are never omittable, even where some tools would allow it.index resolution): import {} from "./dir" resolving to ./dir/index.ts — supported where the runtime context allows it. A directory can also carry its own package.json with "main"/"types" (and "typesVersions"), which take precedence over a bare index.js lookup. Distinct from — and more limited than — full node_modules package resolution; Node.js itself considers directory-as-module a legacy feature.paths: overrides resolution for bare specifiers with an explicit path-mapping table, supporting a single * wildcard per pattern and multiple fallback candidates per key. When several patterns match one specifier, the one with the longest literal prefix before the wildcard wins. paths does not change emitted import paths at all — it's purely a TypeScript-side resolution hint, so a paths alias the runtime/bundler doesn't also know about will type-check fine and crash at runtime. paths resolves relative to baseUrl if set, otherwise relative to the tsconfig.json location. Never point paths at a node_modules package or a monorepo sibling package directly — doing so bypasses that package's real package.json (exports/main/types/typesVersions) entirely; use real node_modules symlinking (npm/yarn/pnpm workspaces) instead so both TypeScript and the runtime see the same resolution.baseUrl: a legacy (AMD-era) option specifying a directory bare specifiers resolve from — lower precedence than node_modules lookups where those are supported, and no longer needed just to set paths's resolution root (as of TS 4.1). Never affects relative specifier resolution.node_modules package lookups: for every ancestor directory of the importing file that has a node_modules folder, TypeScript checks (in order) a same-named package directory, then a same-named @types package directory, trying to resolve types first across all ancestors before a second pass allows resolving to a plain .js file. Every moduleResolution mode except classic supports this (classic only checks node_modules/@types, never real package directories)."exports" (read under node16/nodenext/bundler, unless resolvePackageJsonExports is disabled): TypeScript follows Node's "exports" resolution algorithm exactly to map a subpath request to a file, matching "types"/"default" conditions (plus versioned "types@{selector}" conditions, and any custom conditions from customConditions) before falling back to other conditions the mode specifies (import/require/etc.). Once "exports" is present at all, any subpath not explicitly matched by it is unresolvable — even a path that would have worked via plain file lookup without "exports" present."typesVersions": lets a package ship different .d.ts sets for different TypeScript compiler versions (e.g. new syntax in one set, a downleveled-for-compatibility set for older TS) — supported in every moduleResolution mode, but only consulted when "exports" isn't the thing driving resolution for that request.// tsconfig.json
{ "compilerOptions": { "paths": { "@app/*": ["./src/*"] } } }
import { Button } from "@app/components/Button"; // resolves to ./src/components/Button
paths wildcard alias mapping a bare specifier prefix to a relative source directory — purely a TypeScript-side convenience that a bundler must be configured to mirror separately.| Feature | Purpose | Caveat |
|---|---|---|
| Extension substitution | prefer .ts/.d.ts over .js for the same logical path | happens regardless of the extension literally written in the import |
paths | remap bare specifiers to local paths | doesn't affect emitted JS — must match the real runtime/bundler config separately |
baseUrl | resolution root for bare specifiers | legacy (AMD-era); not required for paths since TS 4.1 |
node_modules lookup | standard package resolution | unsupported in classic mode except for @types |
package.json "exports" | Node-spec-accurate subpath/conditional resolution | blocks any subpath not explicitly listed once present at all |
package.json "typesVersions" | per-TS-version type definitions | ignored when "exports" handles the request instead |
moduleResolution to match the actual runtime/bundler algorithm — it's not a stylistic choice, a mismatch produces types that lie about what will really resolve at runtime.paths as TypeScript-only bookkeeping that must be kept in sync with real bundler/runtime aliasing — never point it at node_modules or monorepo sibling packages; use real workspace symlinking instead."exports" at all is a breaking change for any consumer relying on subpaths not explicitly listed in it — the field's mere presence closes off everything else.module Compiler Option: moduleResolution and module are frequently locked together (node16/node18/nodenext for module imply a matching moduleResolution)..d.ts resolution is exactly what extension substitution and "types" conditions exist to prioritize.module/moduleResolution together for a given target (Node.js, bundler, library).