ماژول‌ها در TypeScript یکی از بنیادی‌ترین مفاهیم در معماری پروژه‌های مدرن هستند که بر نگهداشت‌پذیری، Tree Shaking، Type Safety و تعامل با اکوسیستم JavaScript اثر می‌گذارند. Module در TypeScript، فراتر از یک ساختار نحوی برای import و export است؛ یک سیستم کامل با Resolution Algorithm، Module Graph، Interop Rules و Compilation Strategies است. درک عمیق این سیستم، پیش‌نیاز طراحی پروژه‌های مقیاس‌پذیر، جلوگیری از Circular Dependency، و بهینه‌سازی Bundle Size است. در این راهنما، مکانیزم دقیق Module System در TypeScript از منظر Resolution، Interop، Module Graph و الگوهای پیشرفته بررسی می‌شود.

در یکی از پروژه‌های سازمانی، تیمی پس از مهاجرت از CommonJS به ESM، با خطاهای زمان اجرا مواجه شد که ناشی از Interop نادرست بین دو سیستم بود. این تجربه نشان می‌دهد که Module System یک مسئله‌ی مهندسی است، نه یک تصمیم نحوی.

Module در TypeScript چیست

Module در TypeScript یک فایل مستقل با Scope اختصاصی است که از طریق import و export با سایر Moduleها تعامل می‌کند. تفاوت Module با Script (کد بدون Import/Export) در این است که Script در Global Scope اجرا می‌شود، اما Module Scope اختصاصی دارد.

ویژگی‌های کلیدی Module:

  • Scope Isolation: متغیرهای یک Module از خارج قابل دسترسی نیستند.
  • Explicit Dependencies: وابستگی‌ها با import مشخص می‌شوند.
  • Static Analysis: تحلیل در زمان کامپایل.
  • Hoisting: Importها پیش از اجرا پردازش می‌شوند.
  • Singleton: هر Module یک بار ارزیابی می‌شود.
  • Live Bindings: در ESM، Importها به‌صورت زنده به‌روزرسانی می‌شوند.

در TypeScript، هر فایلی که حداقل یک import یا export داشته باشد، به‌عنوان Module در نظر گرفته می‌شود.

سیستم‌های Module: ESM، CommonJS، AMD، UMD

سیستم Syntax Loading محیط اصلی
ESM import/export Static Browser، Node.js 14+
CommonJS require/module.exports Runtime Node.js
AMD define/require Async RequireJS
UMD ترکیبی Universal کتابخانه‌های عمومی
System System.register Dynamic SystemJS

ESM در مقابل CommonJS

ویژگی ESM CommonJS
Loading Static Dynamic
Tree Shaking پشتیبانی محدود
Top-Level Await پشتیبانی ندارد
Live Bindings دارد ندارد
Circular Dependency مقاوم‌تر آسیب‌پذیرتر
Async Loading import() Promise
Browser بومی نیاز به Bundler

Resolution Algorithm

Module Resolution فرآیند تبدیل Specifier در import به مسیر فایل واقعی است. TypeScript چند استراتژی دارد:

۱. Classic

استراتژی قدیمی که برای پروژه‌های ساده طراحی شده است. از node_modules پشتیبانی نمی‌کند.

۲. Node

استراتژی مشابه Node.js. از node_modules، package.json و index.ts پشتیبانی می‌کند.

۳. NodeNext

استراتژی مدرن که از exports و imports در package.json پشتیبانی می‌کند.

۴. Bundler

استراتژی برای Bundlerهایی مانند Vite، Webpack و esbuild. اجازه می‌دهد بدون Extension در import بنویسید.

// tsconfig.json
{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"]
    }
  }
}

الگوریتم Resolution در Node

برای import { foo } from "./utils/bar"، الگوریتم به‌ترتیب زیر عمل می‌کند:

  1. بررسی ./utils/bar.ts
  2. بررسی ./utils/bar.tsx
  3. بررسی ./utils/bar.d.ts
  4. بررسی ./utils/bar/index.ts
  5. بررسی package.json در ./utils/bar/
  6. جستجو در node_modules

Module Graph و Dependency

Module Graph یک گراف جهت‌دار است که وابستگی‌های Moduleها را نشان می‌دهد:

  • Node: هر Module یک Node است.
  • Edge: هر Import یک Edge است.
  • Entry Point: نقطه‌ی شروع (main.ts یا index.ts).
  • Leaf: Module بدون وابستگی.
  • Cycle: Circular Dependency.

ترتیب ارزیابی

Module Graph به‌صورت Depth-First Traversal ارزیابی می‌شود:

A imports B
B imports C
C imports D

ترتیب ارزیابی:
1. D (leaf)
2. C
3. B
4. A (entry)

Singleton Pattern

هر Module تنها یک بار ارزیابی می‌شود و نتیجه در Module Cache ذخیره می‌شود. این ویژگی، Module را به یک Singleton طبیعی تبدیل می‌کند.

// counter.ts
let count = 0;
export function increment() { return ++count; }

// a.ts
import { increment } from "./counter";
increment(); // 1

// b.ts
import { increment } from "./counter";
increment(); // 2 (نه 1، چون Module Cache)

Interop بین ESM و CommonJS

Interop بین ESM و CommonJS یکی از پیچیده‌ترین جنبه‌های Module System است:

ESM Import از CommonJS

// CommonJS Module
// lib.js
module.exports = { foo: "bar" };
module.exports.baz = "qux";

// ESM Import
import lib from "./lib.js"; // default import کار می‌کند
import { baz } from "./lib.js"; // Named import ممکن است کار کند

CommonJS Import از ESM

// ESM Module
// lib.mjs
export const foo = "bar";
export default "baz";

// CommonJS Import
const lib = require("./lib.mjs"); // نیاز به dynamic import
// یا
const { foo } = await import("./lib.mjs");

esModuleInterop

// tsconfig.json
{
  "compilerOptions": {
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "module": "ESNext"
  }
}

با esModuleInterop، TypeScript امکان import foo from "cjs-module" را فراهم می‌کند.

Compilation Strategy

module target کاربرد
ESNext ES2022 Frontend با Bundler
NodeNext ES2022 Node.js مدرن
CommonJS ES5 Node.js قدیمی
UMD ES5 کتابخانه
System ES5 SystemJS

package.json و Module Field

{
  "name": "my-package",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": {
        "types": "./dist/index.d.ts",
        "default": "./dist/index.js"
      },
      "require": {
        "types": "./dist/index.d.cts",
        "default": "./dist/index.cjs"
      }
    },
    "./utils": {
      "import": "./dist/utils.js",
      "require": "./dist/utils.cjs"
    }
  },
  "sideEffects": false
}

فیلد exports امکان تعریف مسیرهای مختلف برای ESM و CommonJS را فراهم می‌کند.

Type-only Import و Export

// Type-only import
import type { User } from "./types";

// Type-only export
export type { User };

// Mixed
import { type Config, loadConfig } from "./config";

// Inline type
import { type User, fetchUser } from "./api";

مزایای Type-only:

  • حذف از Bundle در زمان کامپایل.
  • جلوگیری از Circular Dependency Type-level.
  • بهبود Tree Shaking.
  • کاهش Bundle Size.

برای مطالعه‌ی بیشتر، پست‌های استفاده از Moduleها در TypeScript و تقسیم کد TypeScript به ماژول‌ها مفید هستند.

Circular Dependency

Circular Dependency زمانی رخ می‌دهد که Module A به B و B به A وابسته باشد.

// a.ts
import { b } from "./b";
export const a = () => b();

// b.ts
import { a } from "./a"; // Circular
export const b = () => a();

پیامدها در ESM

در ESM، Circular Dependency با Live Bindings مدیریت می‌شود. اما اگر به مقدار در زمان ارزیابی دسترسی پیدا کنید، ممکن است undefined بگیرید:

// a.ts
import { b } from "./b";
export const a = "A";
console.log(b); // ممکن است undefined باشد

// b.ts
import { a } from "./a";
export const b = "B";
console.log(a); // ممکن است undefined باشد

پیامدها در CommonJS

در CommonJS، Circular Dependency می‌تواند منجر به مقدار ناقص شود:

// a.js
const b = require("./b");
module.exports.a = "A";
console.log(b); // {} (ناقص)

// b.js
const a = require("./a");
module.exports.b = "B";
console.log(a); // {} (ناقص)

شناسایی و حل

# با madge
npx madge --circular --extensions ts,tsx src/

# با dpdm
npx dpdm --circular src/index.ts

Tree Shaking و Side Effects

Tree Shaking فرآیند حذف کدهای استفاده‌نشده در زمان Bundling است:

  1. استفاده از ESM: Static Analysis ممکن می‌شود.
  2. Named Export: Bundler می‌تواند Exportهای استفاده‌نشده را حذف کند.
  3. Side-effect-free: تعریف "sideEffects": false در package.json.
  4. Pure Functions: با /*#__PURE__*/ در صورت لزوم.
  5. پرهیز از Barrel Files سراسری: Barrel Files Tree Shaking را مختل می‌کنند.
// package.json
{
  "sideEffects": [
    "*.css",
    "./src/polyfills.ts"
  ]
}

برای مطالعه‌ی بیشتر، پست مفاهیم پیشرفته جاوااسکریپت مفید است.

الگوهای پیشرفته

۱. Barrel Export کنترل‌شده

// features/auth/index.ts
export { LoginForm } from "./components/LoginForm";
export { RegisterForm } from "./components/RegisterForm";
export { useAuth } from "./hooks/useAuth";
export type { User, Credentials } from "./types";

۲. Dynamic Import

// Lazy Loading
const module = await import("./heavy-module");
module.doSomething();

// Conditional Import
if (condition) {
  const { feature } = await import("./feature");
}

۳. Re-export با Type

export type { User } from "./types";
export { fetchUser } from "./api";
export * from "./utils";
export * as helpers from "./helpers";

۴. Module Augmentation

// augment.d.ts
declare module "express" {
  interface Request {
    user?: User;
  }
}

پرسش‌های پرتکرار

تفاوت ESM و CommonJS چیست؟

ESM از import/export و Static Loading استفاده می‌کند. CommonJS از require و Dynamic Loading.

چرا Module در TypeScript Singleton است؟

چون Module Cache، هر Module را تنها یک بار ارزیابی می‌کند.

چگونه Circular Dependency را حل کنیم؟

با استخراج Type مشترک، Lazy Import، Dependency Injection یا Event-based Communication.

آیا Type-only Import بر Bundle اثر دارد؟

بله، import type در زمان کامپایل حذف می‌شود.

آیا Tree Shaking در CommonJS کار می‌کند؟

محدود. برای Tree Shaking مؤثر، ESM لازم است.

تفاوت moduleResolution Node و Bundler چیست؟

Node از Extension پشتیبانی می‌کند. Bundler اجازه می‌دهد بدون Extension import بنویسید.

آیا esModuleInterop ضروری است؟

برای Import از CommonJS در ESM، بله.

اشتباهات رایج

اشتباه علت راه‌حل
Circular Dependency عدم لایه‌بندی بازطراحی Dependency Graph
Barrel File سراسری سادگی Import Barrel در سطح Feature
Default Export سراسری عادت قدیمی Named Export
عدم استفاده از import type ناآشنایی Type-only Import
CommonJS در پروژه مدرن عدم مهاجرت ESM
عدم تنظیم sideEffects عدم Tree Shaking "sideEffects": false
عدم هماهنگی module و moduleResolution ناآشنایی ESNext + Bundler
Mixed Import بدون Interop عدم esModuleInterop esModuleInterop: true

ملاحظات پیشرفته

در سطح معماری، Module System نیازمند استراتژی جامع است:

۱. Project References: برای Monorepo با Build Time پایین.

۲. Module Federation: برای Micro-frontend.

۳. Path Mapping: برای Importهای کوتاه.

۴. Dependency Graph Analysis: در CI/CD.

۵. Bundle Analysis: با webpack-bundle-analyzer.

۶. Code Splitting: با Dynamic Import.

۷. Public API Pattern: برای هر Feature.

۸. Interop Rules: مستندسازی قواعد در تیم.

برای مطالعه‌ی بیشتر، پست‌های استفاده از Moduleها در TypeScript، تقسیم کد TypeScript به ماژول‌ها، تایپ اسکریپت از صفر، خطاهای رایج TypeScript و چرا TypeScript کیفیت کد را بالا می‌برد مراجع کاملی هستند.

نتیجه

ماژول‌ها در TypeScript یک سیستم کامل با Resolution Algorithm، Module Graph، Interop Rules و Compilation Strategies هستند. درک عمیق این سیستم، پیش‌نیاز طراحی پروژه‌های مقیاس‌پذیر، جلوگیری از Circular Dependency و بهینه‌سازی Bundle است. انتخاب ESM، Named Export، Type-only Import و تنظیم صحیح module و moduleResolution، ستون‌های یک معماری سالم هستند.

💡 اگر تجربه‌ای در کار با Module System در TypeScript داشته‌اید، برای ما جالب است بدانیم کدام چالش بیشترین زمان را از تیم شما گرفت: Resolution، Interop یا Circular Dependency. تجربه‌ی خودتان را در دیدگاه‌ها بنویسید.