Module Resolution and Path Aliases

Introduction

When you write import { x } from "./utils", TypeScript must resolve that specifier to a file or package. Module resolution rules depend on moduleResolution in tsconfig.json. Path aliases shorten long relative paths (@/components/Button). This chapter explains how resolution works and how to configure aliases safely.

Prerequisites

moduleResolution Overview

SettingTypical use
Node10 / NodeOlder Node resolution (legacy)
Node16 / NodeNextModern Node ESM + package.json "exports"
BundlerVite, Webpack, esbuild—matches bundler behavior

Modern Node projects often use:

json
{
  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "NodeNext"
  }
}

module and moduleResolution should align—check TypeScript handbook when upgrading.

Relative vs Bare Specifiers

typescript
import { add } from "./math.js";
import express from "express";

Code explanation:

  • ./math.js is relative—resolved from the importing file’s directory
  • express is bare—resolved from node_modules using Node or bundler rules

package.json "type" and "exports"

Node ESM vs CJS depends on package metadata:

json
{
  "name": "my-lib",
  "type": "module",
  "exports": {
    ".": "./dist/index.js"
  }
}

TypeScript reads these fields with NodeNext to decide which .d.ts and .js files match an import.

baseUrl and paths (Path Aliases)

Map logical prefixes to folders:

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

Usage:

typescript
import { formatUser } from "@/models/user.js";

Code explanation:

  • @/* maps to src/* for the type checker
  • Runtime/bundler must apply the same mapping (see below)

Warning

paths Alone Does Not Rewrite Runtime

tsc does not rewrite alias paths in emitted JS unless you use a tool that does. Bundlers (Vite) or tsc-alias / Node subpath imports must match.

Vite example (conceptual)

vite.config.ts often mirrors:

typescript
import path from "node:path";
 
export default {
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "src"),
    },
  },
};

TypeScript paths and Vite alias stay in sync.

rootDirs (Monorepo Hint)

Multiple roots treated as one logical tree:

json
{
  "compilerOptions": {
    "rootDirs": ["src", "generated"]
  }
}

Used when generated and hand-written code merge in imports—advanced monorepo setups.

types and typeRoots

json
{
  "compilerOptions": {
    "types": ["node"]
  }
}

Loads @types/node globally. Omit types to include all visible @types packages.

Troubleshooting Resolution Errors

ErrorCommon causeFix
Cannot find module './x'Wrong path or missing extension (NodeNext)Fix relative path; add .js
Cannot find module 'express'Package not installednpm install express + @types/express if needed
Cannot find module '@/foo'paths not configured in toolAdd paths + bundler alias
Types exist but value import failsexport field in dependencyCheck package exports / use correct import

Enable trace (debugging):

bash
npx tsc --traceResolution 2>&1 | head -50

Verbose output shows candidate files—use sparingly.

Subpath Imports via package.json (Node)

Alternative to paths for libraries:

json
{
  "imports": {
    "#utils/*": "./src/utils/*"
  }
}

Node 20+ supports import maps in apps; align TypeScript with moduleResolution: "NodeNext" and proper typings.

Practical tsconfig Fragment

Bundler fits Vite/React apps; Node services prefer NodeNext.

FAQ

NodeNext vs Bundler?

NodeNext for tsc + node runs. Bundler when a bundler compiles and you want matching resolution.

Why .js in paths mapping?

"@/*": ["src/*"] maps to .ts sources; imports still often use .js suffix for Node ESM emit.

Do path aliases work in tests?

Jest/Vitest need their own moduleNameMapper / resolve.alias matching paths.

Can paths point outside project?

Yes, but prefer local src aliases; crossing repos needs project references.

project references?

Separate tsconfig per package with references—monorepo topic beyond this chapter.

Relative import hell?

Aliases help; also colocate files and avoid deep ../../../ when structure allows.