Effective TypeScript path mapping module resolution requires a strict separation between compile-time type checking and runtime execution environments. Misalignment between these layers is the leading cause of broken imports, silent type errors, and failed package distributions. This guide establishes production-grade patterns for alias architecture, build-time transformation, and CI validation, operating within the broader TypeScript Configuration & Build Tooling ecosystem.

Compiler vs Runtime Resolution Mechanics

TypeScript’s paths directive operates exclusively during type-checking and compilation. It instructs the compiler how to locate source files for IntelliSense and type validation, but it performs zero runtime resolution or AST rewriting. Conversely, Node.js relies on native resolution algorithms: ESM uses package.json exports/imports fields, while CommonJS (CJS) falls back to require() resolution order (node_modules, NODE_PATH, relative paths).

The moduleResolution compiler option dictates how closely TypeScript mimics these runtime behaviors. Legacy node mode ignores conditional exports and extension requirements. Modern node16/nodenext modes enforce ESM resolution rules, including explicit .js extensions and strict exports mapping. The bundler mode assumes a downstream tool (Vite, Webpack, esbuild) will handle resolution, which is dangerous for library authors shipping directly to Node.

{
  "compilerOptions": {
    "module": "Node16",
    "moduleResolution": "Node16",
    "paths": {
      "@core/*": ["./src/core/*"]
    }
  }
}

HAZARD PREVENTION: Never pair moduleResolution: "bundler" with a library targeting native Node execution. The compiler will silently accept missing .js extensions and unexported paths, causing ERR_MODULE_NOT_FOUND at runtime. Align moduleResolution strictly with your target runtime.

Strategic paths and baseUrl Architecture

Maintainable aliasing requires strict scoping and collision avoidance. Mapping aliases to vendor-specific namespaces (e.g., @internal/*, @pkg/*) prevents shadowing third-party dependencies. Root-level baseUrl mapping without explicit paths prefixes forces TypeScript to resolve bare imports against your source tree first, creating implicit collisions with node_modules packages.

Granularity matters: prefer directory-level wildcards ("@utils/*": ["./src/utils/*"]) over file-level mappings to reduce configuration drift. Cross-platform normalization is handled automatically by TypeScript’s POSIX-to-Windows path translation, but rootDirs can introduce ambiguity when merging output directories. Baseline compiler settings that complement these strategies are detailed in Optimizing tsconfig.json for Library Distribution.

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@lib/*": ["./src/lib/*"],
      "@types/*": ["./src/types/*"]
    }
  }
}

HAZARD PREVENTION: Avoid "baseUrl": "src" with "paths": { "*": ["*"] }. This forces the compiler to resolve every bare import against your source tree first, causing import { lodash } from "lodash" to fail if a local src/lodash.ts exists. Always scope aliases under a dedicated prefix.

Build-Time Alias Transformation Pipelines

Since TypeScript does not rewrite paths in emitted JavaScript, build-time AST transformation is mandatory for distribution. Regex-based string replacement is fragile and breaks sourcemaps, declaration files, and minified outputs. Use AST-aware bundler plugins (tsup, rollup-plugin-alias, esbuild) that traverse the syntax tree and rewrite import specifiers safely.

Type-only imports (import type { X } from "@lib/x") are stripped during declaration emission but must still resolve correctly during the initial compilation pass. When generating .d.ts files, the transformer must preserve or rewrite type paths consistently to avoid broken consumer references. The mechanics of this process are covered in Declaration File Generation and Type Stripping.

// tsup.config.ts
import { defineConfig } from "tsup";
import alias from "rollup-plugin-alias";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["cjs", "esm"],
  dts: true,
  sourcemap: true,
  plugins: [
    alias({
      entries: [
        { find: /^@lib\/(.*)/, replacement: "./src/lib/$1" },
      ],
    }),
  ],
});

HAZARD PREVENTION: Always enable sourcemap: true and verify that alias transformations do not strip import type statements prematurely. Use tsc --declarationMap alongside your bundler to maintain IDE navigation in downstream projects.

CI/CD Resolution Validation Workflows

Automated pre-publish checks must detect resolution mismatches before artifacts reach the registry. Run isolated tsc --noEmit in clean CI runners without local caches to catch missing type references. Matrix test across Node.js LTS versions and package managers (npm, pnpm, yarn) to expose hoisting and resolution order discrepancies.

Custom AST scanners can lint for unresolved aliases by parsing the tsconfig.json paths and verifying every import matches a defined pattern. Automated smoke tests should execute both ESM and CJS entry points to validate runtime resolution chains.

# .github/workflows/resolution-validation.yml
name: Resolution Validation
on: [push, pull_request]
jobs:
  validate:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with:
          version: 9
      - run: pnpm install --frozen-lockfile
      - name: Type Check (Clean)
        run: pnpm tsc --noEmit --force
      - name: Smoke Test Dual Entry
        run: |
          node --input-type=module -e "import '@lib/core'; console.log('ESM OK')"
          node -e "require('@lib/core'); console.log('CJS OK')"

HAZARD PREVENTION: CI caches often mask node_modules resolution failures. Always run pnpm install --frozen-lockfile or npm ci with --force in validation jobs to guarantee a pristine dependency graph.

Dual ESM/CJS Export Mapping

Shipping both module formats requires explicit conditional exports to guide runtimes and type checkers. Map .mjs outputs to import conditions and .cjs to require. ESM mandates explicit .js extensions in source imports, even when referencing .ts files. Fallback chains ("default") must be ordered carefully to prevent bundlers from selecting the wrong format.

Consumer-side type resolution depends on the types field pointing to a valid .d.ts entry point. Misaligned exports conditions cause TypeScript to fall back to legacy main/module fields, breaking modern resolution. For comprehensive strategies on bridging runtime mechanics with downstream compatibility, see Handling TypeScript Path Aliases in Published Packages.

{
  "name": "@scope/library",
  "type": "module",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.mjs",
      "require": "./dist/index.cjs",
      "default": "./dist/index.mjs"
    },
    "./utils": {
      "types": "./dist/utils.d.ts",
      "import": "./dist/utils.mjs",
      "require": "./dist/utils.cjs"
    }
  }
}

HAZARD PREVENTION: Never omit the types condition in exports. TypeScript’s moduleResolution: "node16" will fail to locate declarations if types is not explicitly mapped, causing Cannot find module errors for consumers.

Common Pitfalls & Resolution Matrix

Issue Root Cause Resolution
Publishing packages with unresolved paths aliases TypeScript emits raw alias strings in .js output without runtime transformation Implement build-time path rewriting plugins and enforce tsc --noEmit validation in CI before publishing.
Mismatched moduleResolution between TS config and bundler Compiler assumes bundler resolution while runtime expects strict Node16 rules Align moduleResolution to node16 or nodenext and explicitly configure bundler resolution fields to match runtime behavior.
Overusing baseUrl causing implicit import collisions Root-level mapping forces bare imports to resolve against source tree before node_modules Scope aliases under a dedicated namespace (e.g., @lib/*) and avoid mapping root directories to prevent ambiguous resolution.
ESM runtime failing to resolve .js extensions mapped from .ts sources Node ESM requires explicit extensions; TS paths does not auto-append them Configure explicit exports conditions and enforce .js extensions in source imports when targeting native ESM execution.