Open-Source Wikis

/

TypeScript

/

Systems

/

Module resolution

microsoft/TypeScript

Module resolution

How TypeScript turns an import specifier or require call into a resolved file path. Handles five module-resolution strategies (classic, node10/node, node16, nodenext, bundler) plus the related dance of paths, baseUrl, package.json exports/imports, and conditional exports.

Source

File Lines Role
src/compiler/moduleNameResolver.ts 3,428 The resolution algorithms
src/compiler/resolutionCache.ts 2,000+ Memoisation across Program rebuilds
src/compiler/moduleSpecifiers.ts 1,800+ The inverse — generate import specifiers (used by auto-import / refactor)
src/server/packageJsonCache.ts small tsserver-side package.json caching
src/server/moduleSpecifierCache.ts small Cache for auto-import specifier generation

Purpose

When the parser encounters import { foo } from "bar", the program needs to find bar. That means:

  1. Apply --paths and --baseUrl rewrites.
  2. If bar is relative, look for bar.ts, bar.tsx, bar.d.ts, then maybe bar/index.ts, then bar.js, bar.jsx (when allowJs).
  3. If bar is bare, walk up node_modules/, consulting package.json's types, typings, exports, and imports.
  4. Apply conditions (import/require/types/browser/development/etc.) per the resolution strategy.
  5. Honour .cts/.mts/.ts/.tsx and the inferred CommonJS-vs-ESM mode.
  6. If everything fails, fall through to the JavaScript file (when allowJs) or report a diagnostic.

Resolution modes

Mode When to use Notes
classic TypeScript 1.5–era resolution. Walks parent directories for relative imports. Legacy. Avoid in new projects.
node10 (alias node) Original Node.js CommonJS behaviour. Default for --module commonjs.
node16 Node 16+ ESM/CJS dual mode with package.json#exports. For Node-targeted code.
nodenext Same as node16 but tracks newer Node versions. Recommended for new Node projects.
bundler Like node10 but allows extension-less imports and assumes a bundler resolves further. For Webpack/Vite/esbuild projects.

The implementation files generally have one entry function per strategy: nodeModuleNameResolver, bundlerModuleNameResolver, classicNameResolver. Each returns a ResolvedModuleWithFailedLookupLocations that tells the caller (a) the resolved file, and (b) the locations where it looked — used for --traceResolution output and for invalidating watchers when something appears at a previously-looked-up location.

Key abstractions

Symbol Role
resolveModuleName(moduleName, containingFile, options, host, cache?, redirectedReference?) Public entry — picks the strategy and returns ResolvedModuleWithFailedLookupLocations
nodeModuleNameResolver, bundlerModuleNameResolver, classicNameResolver Per-strategy implementations
resolveTypeReferenceDirective Resolves /// <reference types=… /> and types/typeRoots
getPackageScopeForPath, readPackageJsonInfo package.json lookups
ModuleResolutionCache, TypeReferenceDirectiveResolutionCache Per-program caches
createResolutionCache (in resolutionCache.ts) Coordinates caches across watch rebuilds and invalidates them when files appear/disappear

How it works

graph TD
    Imp["import 'pkg'"] --> Resolve["resolveModuleName"]
    Resolve --> Strat{"strategy?"}
    Strat -->|node16/nodenext| Node["node-style + exports/conditions"]
    Strat -->|node10| Node10["legacy node resolution"]
    Strat -->|classic| Classic["walk parents for files"]
    Strat -->|bundler| Bundler["node-style without strict ext check"]
    Node --> Cache["ModuleResolutionCache"]
    Node10 --> Cache
    Classic --> Cache
    Bundler --> Cache
    Cache --> Result["ResolvedModuleWithFailedLookupLocations"]
    Result --> ProgramMR["program.ts processes imports"]
    Result --> Watch["resolutionCache schedules invalidation watchers"]

processImportedModules in program.ts loops over every import in every source file and calls resolveModuleName. The failedLookupLocations returned with each result drive watch-mode invalidation: if any of those paths appears later (e.g., a missing node_modules/foo is finally installed), the program rebuilds.

package.json exports

In node16/nodenext mode, exports substantially changes resolution. A package's exports field is a tree keyed on subpath patterns, where each leaf is a tree keyed on conditions (import, require, types, default, custom). The resolver walks both trees per the Node spec. The bundler mode applies a simplified version that prefers import first.

paths and baseUrl

compilerOptions.paths lets users alias module names ("@app/*": ["src/*"]). The resolver applies these aliases first, then attempts the standard resolution against each candidate. baseUrl is the implicit root for these aliases.

Inverse: generating specifiers

When the language service needs to suggest an auto-import, it must generate the specifier a user should type, not just the resolved file. moduleSpecifiers.ts is the inverse of resolution. It tries paths, then baseUrl, then node_modules package names (preserving package.json#exports shapes), then relative paths, choosing the shortest stable form.

Diagnostics and tracing

--traceResolution writes a verbose log of every resolution attempt: which files were probed, which package.json fields were consulted, which conditions matched. This is the primary debugging tool when "Cannot find module" appears unexpectedly.

Integration points

  • Called from program.ts via processImportedModules when building the file closure.
  • Called from tsserver's editorServices.ts when an import changes.
  • Called from the language service's auto-import feature (src/services/codefixes/importFixes.ts, among others).
  • Cached results are kept in resolutionCache.ts and shared across program rebuilds when possible.

Entry points for modification

See features/modules for the user-facing module-system story.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Module resolution – TypeScript wiki | Factory