TypeScript — Module Resolution
Module resolution is how TypeScript determines which file an import statement refers to. Understanding resolution strategies is critical for configuring projects that work correctly across Node.js, bundlers, and different module systems. The wrong strategy can cause cryptic errors like "Cannot find module" or "File is not a module."
| 1 | // CommonJS — Node.js original module system |
| 2 | // Uses require() and module.exports |
| 3 | // Synchronous, runtime resolution |
| 4 | |
| 5 | // math.ts (compiled to CommonJS) |
| 6 | export function add(a: number, b: number): number { |
| 7 | return a + b; |
| 8 | } |
| 9 | |
| 10 | export const PI = 3.14159; |
| 11 | |
| 12 | // Compiled output: |
| 13 | // "use strict"; |
| 14 | // Object.defineProperty(exports, "__esModule", { value: true }); |
| 15 | // exports.PI = void 0; |
| 16 | // function add(a, b) { return a + b; } |
| 17 | // exports.add = add; |
| 18 | // exports.PI = 3.14159; |
| 19 | |
| 20 | // ESM — ECMAScript standard |
| 21 | // Uses import/export, static analysis |
| 22 | // Asynchronous, supports top-level await |
| 23 | |
| 24 | // math.ts (compiled to ESM) |
| 25 | export function subtract(a: number, b: number): number { |
| 26 | return a - b; |
| 27 | } |
| 28 | |
| 29 | export const TAU = 2 * Math.PI; |
| 30 | |
| 31 | // Compiled output: |
| 32 | // export function subtract(a, b) { return a - b; } |
| 33 | // export const TAU = 2 * Math.PI; |
| 34 | |
| 35 | // Dynamic import — works in both systems |
| 36 | async function loadModule() { |
| 37 | const { add } = await import("./math"); |
| 38 | console.log(add(1, 2)); |
| 39 | } |
| 40 | |
| 41 | // CommonJS require vs ESM import |
| 42 | const lodash = require("lodash"); // CommonJS |
| 43 | import _ from "lodash-es"; // ESM |
| 44 | import type { lodash } from "lodash"; // TypeScript type-only |
info
| 1 | { |
| 2 | "compilerOptions": { |
| 3 | "module": "Node16", |
| 4 | "moduleResolution": "Node16", |
| 5 | "target": "ES2022", |
| 6 | "outDir": "./dist", |
| 7 | "rootDir": "./src" |
| 8 | } |
| 9 | } |
| 1 | // "moduleResolution": "node" — Classic Node.js resolution |
| 2 | // - Looks in node_modules |
| 3 | // - Follows package.json "main" field |
| 4 | // - Supports index.ts in directories |
| 5 | // - Does NOT respect package.json "exports" |
| 6 | |
| 7 | // "moduleResolution": "node16" — Modern Node.js resolution |
| 8 | // - Respects package.json "exports" field |
| 9 | // - Supports dual CJS/ESM packages |
| 10 | // - Requires explicit .js extensions in imports |
| 11 | // - Enforces ESM/CJS interop rules |
| 12 | |
| 13 | // "moduleResolution": "bundler" — For bundled environments |
| 14 | // - Most lenient — lets the bundler resolve |
| 15 | // - Supports index.ts, extensionless imports |
| 16 | // - Supports package.json "exports" |
| 17 | // - Best for Vite, webpack, esbuild, Rollup |
| 18 | |
| 19 | // "moduleResolution": "nodenext" — Same as node16 (alias) |
| 20 | |
| 21 | // Comparison: |
| 22 | // Strategy | extensions | exports field | barrel re-exports |
| 23 | // node | optional | no | yes |
| 24 | // node16/nodenext| required | yes | no |
| 25 | // bundler | optional | yes | yes |
| 26 | |
| 27 | // Import patterns per strategy: |
| 28 | // node16 — requires .js extension |
| 29 | import { add } from "./math.js"; |
| 30 | |
| 31 | // bundler — extension optional |
| 32 | import { add } from "./math"; |
| 33 | |
| 34 | // node — extension optional, no exports support |
| 35 | import { add } from "./math"; |
| 1 | { |
| 2 | "compilerOptions": { |
| 3 | "baseUrl": ".", |
| 4 | "paths": { |
| 5 | "@/*": ["./src/*"], |
| 6 | "@components/*": ["./src/components/*"], |
| 7 | "@lib/*": ["./src/lib/*"], |
| 8 | "@types/*": ["./src/types/*"] |
| 9 | } |
| 10 | } |
| 11 | } |
| 1 | // Without path mapping — relative hell |
| 2 | import { Button } from "../../../components/Button"; |
| 3 | import { formatDate } from "../../../lib/utils"; |
| 4 | import { User } from "../../../types/models"; |
| 5 | |
| 6 | // With path mapping — clean imports |
| 7 | import { Button } from "@/components/Button"; |
| 8 | import { formatDate } from "@/lib/utils"; |
| 9 | import { User } from "@/types/models"; |
| 10 | |
| 11 | // Using aliases with barrel exports |
| 12 | // src/components/index.ts |
| 13 | export { Button } from "./Button"; |
| 14 | export { Card } from "./Card"; |
| 15 | export { Modal } from "./Modal"; |
| 16 | |
| 17 | // Clean import via barrel |
| 18 | import { Button, Card, Modal } from "@/components"; |
| 19 | |
| 20 | // Pattern for monorepo path mapping |
| 21 | { |
| 22 | "paths": { |
| 23 | "@app/*": ["./packages/app/src/*"], |
| 24 | "@shared/*": ["./packages/shared/src/*"], |
| 25 | "@ui/*": ["./packages/ui/src/*"] |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | // Vite also needs matching vite.config.ts |
| 30 | // import { defineConfig } from "vite"; |
| 31 | // import tsconfigPaths from "vite-tsconfig-paths"; |
| 32 | // export default defineConfig({ plugins: [tsconfigPaths()] }); |
| 33 | |
| 34 | // Webpack needs matching resolve.alias in webpack.config.js |
| 35 | // resolve: { |
| 36 | // alias: { |
| 37 | // "@": path.resolve(__dirname, "./src"), |
| 38 | // "@components": path.resolve(__dirname, "./src/components"), |
| 39 | // } |
| 40 | // } |
warning
| 1 | // src/components/index.ts — barrel file |
| 2 | // Re-exports all components from a single entry point |
| 3 | |
| 4 | export { Button } from "./Button"; |
| 5 | export { Card } from "./Card"; |
| 6 | export { Modal } from "./Modal"; |
| 7 | export { Input } from "./Input"; |
| 8 | export { Select } from "./Select"; |
| 9 | |
| 10 | // Re-export everything from a module |
| 11 | export * from "./formatters"; |
| 12 | export * from "./validators"; |
| 13 | |
| 14 | // Selective re-export (picking specific items) |
| 15 | export { validateEmail, validatePassword } from "./validators"; |
| 16 | |
| 17 | // Type-only re-export |
| 18 | export type { ButtonProps } from "./Button"; |
| 19 | export type { ModalProps } from "./Modal"; |
| 20 | |
| 21 | // Usage — consumers import from barrel |
| 22 | import { Button, Card } from "@/components"; |
| 23 | import type { ButtonProps } from "@/components"; |
| 24 | |
| 25 | // Barrel with namespace |
| 26 | // src/utils/index.ts |
| 27 | import * as dateUtils from "./dates"; |
| 28 | import * as stringUtils from "./strings"; |
| 29 | import * as arrayUtils from "./arrays"; |
| 30 | |
| 31 | export { dateUtils, stringUtils, arrayUtils }; |
| 32 | |
| 33 | // Or flatten into top-level |
| 34 | export * from "./dates"; |
| 35 | export * from "./strings"; |
| 36 | export * from "./arrays"; |
| 37 | |
| 38 | // Advanced barrel with conditional exports |
| 39 | // package.json |
| 40 | { |
| 41 | "exports": { |
| 42 | ".": { |
| 43 | "import": "./dist/esm/index.js", |
| 44 | "require": "./dist/cjs/index.js", |
| 45 | "types": "./dist/types/index.d.ts" |
| 46 | }, |
| 47 | "./utils": { |
| 48 | "import": "./dist/esm/utils.js", |
| 49 | "require": "./dist/cjs/utils.js", |
| 50 | "types": "./dist/types/utils/index.d.ts" |
| 51 | } |
| 52 | } |
| 53 | } |
warning
| 1 | // Ambient module declarations for untyped packages |
| 2 | // types/legacy-lib.d.ts |
| 3 | declare module "legacy-lib" { |
| 4 | export function doSomething(input: string): number; |
| 5 | export function doAsync(input: string): Promise<number>; |
| 6 | export const VERSION: string; |
| 7 | } |
| 8 | |
| 9 | // Global type declarations |
| 10 | // types/global.d.ts |
| 11 | declare global { |
| 12 | interface Window { |
| 13 | __APP_CONFIG__: { |
| 14 | apiUrl: string; |
| 15 | environment: "development" | "staging" | "production"; |
| 16 | }; |
| 17 | } |
| 18 | |
| 19 | // Augment Node.js globals |
| 20 | namespace NodeJS { |
| 21 | interface ProcessEnv { |
| 22 | NODE_ENV: "development" | "production" | "test"; |
| 23 | DATABASE_URL: string; |
| 24 | API_KEY: string; |
| 25 | } |
| 26 | } |
| 27 | } |
| 28 | |
| 29 | export {}; |
| 30 | |
| 31 | // Module augmentation — extend existing modules |
| 32 | // types/express.d.ts |
| 33 | import { User } from "@/types/models"; |
| 34 | |
| 35 | declare module "express-serve-static-core" { |
| 36 | interface Request { |
| 37 | user?: User; |
| 38 | sessionId?: string; |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | // Declaration merging for CSS modules |
| 43 | declare module "*.module.css" { |
| 44 | const classes: Record<string, string>; |
| 45 | export default classes; |
| 46 | } |
| 47 | |
| 48 | declare module "*.module.scss" { |
| 49 | const classes: Record<string, string>; |
| 50 | export default classes; |
| 51 | } |
| 52 | |
| 53 | // Asset declarations |
| 54 | declare module "*.svg" { |
| 55 | const content: React.FC<React.SVGProps<SVGSVGElement>>; |
| 56 | export default content; |
| 57 | } |
| 58 | |
| 59 | declare module "*.png" { |
| 60 | const src: string; |
| 61 | export default src; |
| 62 | } |
| 1 | { |
| 2 | "name": "@myorg/ui-components", |
| 3 | "version": "2.0.0", |
| 4 | "type": "module", |
| 5 | "exports": { |
| 6 | ".": { |
| 7 | "import": { |
| 8 | "types": "./dist/index.d.mts", |
| 9 | "default": "./dist/index.mjs" |
| 10 | }, |
| 11 | "require": { |
| 12 | "types": "./dist/index.d.cts", |
| 13 | "default": "./dist/index.cjs" |
| 14 | } |
| 15 | }, |
| 16 | "./styles": "./dist/styles.css", |
| 17 | "./icons": { |
| 18 | "import": { |
| 19 | "types": "./dist/icons.d.mts", |
| 20 | "default": "./dist/icons.mjs" |
| 21 | } |
| 22 | } |
| 23 | }, |
| 24 | "types": "./dist/index.d.ts", |
| 25 | "main": "./dist/index.cjs", |
| 26 | "module": "./dist/index.mjs" |
| 27 | } |
info
1. Use "moduleResolution": "bundler" for Vite/webpack projects and "node16" for pure Node.js packages.
2. Always add .js extensions in imports when using node16 resolution — even for TypeScript files.
3. Use path aliases (@/*) to avoid deep relative imports like ../../../.
4. Keep barrel files small and focused. For large packages, use subpath exports (@myorg/ui/icons).
5. Use export type for type-only re-exports to prevent runtime side effects.
6. Use module augmentation (declare module) for untyped third-party packages instead of modifying node_modules.
7. Set "type": "module" in package.json for new projects — ESM is the standard going forward.