استفاده از Moduleها در TypeScript یکی از بنیادی‌ترین تصمیمات معماری در پروژه‌های مقیاس‌پذیر است که بر نگهداشت‌پذیری، مقیاس‌پذیری، Tree Shaking و سرعت Build اثر می‌گذارد. Module در TypeScript یک واحد مستقل از کد است که می‌تواند شامل توابع، کلاس‌ها، Interfaceها و متغیرها باشد و از طریق import و export با سایر Moduleها تعامل کند. این معماری، بر پایه‌ی استانداردهای ES Modules (ESM) و CommonJS شکل گرفته و توسط Module Resolution در TypeScript مدیریت می‌شود. درک عمیق Module System، پیش‌نیاز طراحی یک Dependency Graph سالم، جلوگیری از Circular Dependency و بهینه‌سازی Bundle Size است. در این راهنما، معماری Module در TypeScript از منظر مهندسی نرم‌افزار، Resolution، Bundling و الگوهای پیشرفته بررسی می‌شود.

در یکی از پروژه‌های سازمانی، پس از رسیدن به ۵۰۰ فایل TypeScript، تعداد Circular Dependencyها به‌شدت افزایش یافت و Build Time سه برابر شد. با بازطراحی Dependency Graph و استفاده از Barrel Exports کنترل‌شده، زمان Build به سطح قبلی بازگشت. این تجربه نشان می‌دهد که Module Architecture یک مسئله‌ی مهندسی است، نه یک تصمیم نحوی.

Module در TypeScript چیست

Module در TypeScript یک فایل مستقل است که دارای Scope اختصاصی است. هر چیزی که در یک Module تعریف می‌شود، به‌صورت پیش‌فرض Private است و تنها از طریق export قابل دسترسی می‌شود. این معماری، از آلودگی Global Scope و Name Collision جلوگیری می‌کند.

ویژگی‌های Module در TypeScript:

  • Scope Isolation: هر Module Scope اختصاصی دارد.
  • Explicit Dependencies: وابستگی‌ها با import مشخص می‌شوند.
  • Encapsulation: پیاده‌سازی داخلی از خارج پنهان است.
  • Static Analysis: امکان تحلیل در زمان کامپایل.
  • Tree Shaking: حذف کدهای استفاده‌نشده در Bundle.
  • Lazy Loading: امکان بارگذاری دینامیک.

سیستم‌های Module: ESM و CommonJS

TypeScript از چند سیستم Module پشتیبانی می‌کند که هرکدام ویژگی‌های متفاوتی دارند:

ویژگی ESM CommonJS
Syntax import/export require/module.exports
Loading Static (Top-Level) Dynamic (Runtime)
Tree Shaking پشتیبانی می‌شود محدود
Async Loading بومی (import()) با Promise
Browser بومی نیاز به Bundler
Node.js پشتیبانی (از نسخه ۱۳+) بومی
Top-Level Await پشتیبانی ندارد
Circular Dependency مقاوم‌تر آسیب‌پذیرتر

تنظیم module در tsconfig.json

{
  "compilerOptions": {
    "module": "ESNext",
    "target": "ES2022",
    "moduleResolution": "Bundler",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "resolveJsonModule": true,
    "isolatedModules": true
  }
}

انتخاب module: "ESNext" و moduleResolution: "Bundler"، تنظیمات مدرن برای پروژه‌های Frontend است. برای Node.js، module: "NodeNext" توصیه می‌شود.

Module Resolution

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

۱. Classic

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

۲. Node

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

۳. NodeNext

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

۴. Bundler

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

// با moduleResolution: "Bundler"
import { foo } from "./utils/foo";

// با moduleResolution: "NodeNext"
import { foo } from "./utils/foo.js";

Path Mapping

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"]
    }
  }
}
// استفاده
import { Button } from "@components/Button";
import { formatDate } from "@utils/date";

الگوهای Import و Export

Named Export (توصیه‌شده)

// utils/math.ts
export function add(a: number, b: number): number {
  return a + b;
}

export function multiply(a: number, b: number): number {
  return a * b;
}

// مصرف
import { add, multiply } from "./utils/math";

Named Export مزایای زیر را دارد:

  • Tree Shaking بهتر.
  • Refactoring ساده‌تر.
  • کشف نام در IDE.
  • جلوگیری از Name Collision.

Default Export

// components/Button.tsx
export default function Button() { ... }

// مصرف
import Button from "./components/Button";

Default Export برای کامپوننت‌ها رایج است، اما در پروژه‌های بزرگ توصیه می‌شود از Named Export استفاده شود.

Re-export (Barrel File)

// components/index.ts
export { Button } from "./Button";
export { Input } from "./Input";
export { Card } from "./Card";

// مصرف
import { Button, Input, Card } from "@/components";

Barrel Files سادگی Import را فراهم می‌کنند، اما در پروژه‌های بزرگ می‌توانند به:

  • Circular Dependency منجر شوند.
  • Tree Shaking را مختل کنند.
  • Build Time را افزایش دهند.

توصیه‌ی عملی: Barrel File را در سطح Feature استفاده کنید، نه در سطح کل پروژه.

Type-only Export

// types.ts
export type { User } from "./user";
export interface Config { ... }

// مصرف
import type { User } from "./types";
import { type Config, loadConfig } from "./config";

استفاده از import type باعث می‌شود که TypeScript در زمان کامپایل، این Import را حذف کند و به Bundle اضافه نشود.

Dependency Graph و Circular Dependency

Dependency Graph نمایش گرافیکی از وابستگی‌های Moduleها است. یک Dependency Graph سالم:

  • بدون Cycle است.
  • لایه‌بندی مشخص دارد.
  • جهت وابستگی یک‌طرفه است.
  • Coupling کمینه دارد.

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";
export const b = () => a();

پیامدهای Circular Dependency:

  • Runtime Error در ESM (Temporal Dead Zone).
  • مقدار undefined در زمان Import.
  • Build Time بالا.
  • Tree Shaking ناموفق.

شناسایی Circular Dependency

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

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

# با dependency-cruiser
npx depcruise --validate .dependency-cruiser.js src/

حل Circular Dependency

  1. استخراج Type مشترک: انتقال Type به Module سوم.
  2. Lazy Import: استفاده از import().
  3. Dependency Injection: تزریق وابستگی به‌جای Import مستقیم.
  4. Event-based Communication: جایگزینی فراخوانی مستقیم با Event.
  5. لایه‌بندی معماری: تعریف مرزهای واضح.

Tree Shaking و بهینه‌سازی Bundle

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

  1. استفاده از ESM: CommonJS Tree Shaking را محدود می‌کند.
  2. Named Export: Default Export Tree Shaking را سخت‌تر می‌کند.
  3. Side-effect-free: تعریف "sideEffects": false در package.json.
  4. Pure Functions: استفاده از /*#__PURE__*/ در صورت لزوم.
  5. عدم استفاده از Barrel Files سراسری: Barrel Files Tree Shaking را مختل می‌کنند.
// package.json
{
  "sideEffects": false
}

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

معماری Module در پروژه‌های بزرگ

در پروژه‌های بزرگ، معماری Module باید بر اساس اصول زیر طراحی شود:

۱. Feature-based Structure

src/
├── features/
│   ├── auth/
│   │   ├── components/
│   │   ├── hooks/
│   │   ├── services/
│   │   ├── types.ts
│   │   └── index.ts
│   ├── products/
│   └── cart/
├── shared/
│   ├── ui/
│   ├── utils/
│   └── types/
└── app/
    ├── routes/
    └── providers/

۲. Layered Architecture

Presentation Layer → Application Layer → Domain Layer → Infrastructure Layer

جهت وابستگی باید از لایه‌ی بالاتر به پایین‌تر باشد، نه برعکس.

۳. Public API برای هر Feature

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

هر Feature باید یک Public API مشخص داشته باشد و پیاده‌سازی داخلی را پنهان کند.

Module در Monorepo و Workspaces

در Monorepo، Moduleها می‌توانند بین Packageها به اشتراک گذاشته شوند:

// package.json (root)
{
  "workspaces": ["packages/*"]
}

// packages/ui/package.json
{
  "name": "@myapp/ui",
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs",
      "types": "./dist/index.d.ts"
    }
  }
}

در Monorepo، استفاده از Project References در TypeScript توصیه می‌شود:

{
  "compilerOptions": {
    "composite": true,
    "declaration": true
  },
  "references": [
    { "path": "../ui" },
    { "path": "../utils" }
  ]
}

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

تست Moduleها

Module Architecture بر تست‌پذیری اثر می‌گذارد:

  • Unit Test: تست هر Module به‌صورت مستقل.
  • Mocking: با jest.mock() یا Dependency Injection.
  • Integration Test: تست تعامل چند Module.
  • Coverage: اندازه‌گیری پوشش هر Module.
// __tests__/math.test.ts
import { add, multiply } from "../math";

describe("math", () => {
  it("adds numbers", () => {
    expect(add(2, 3)).toBe(5);
  });

  it("multiplies numbers", () => {
    expect(multiply(2, 3)).toBe(6);
  });
});

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

تفاوت ESM و CommonJS در TypeScript چیست؟

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

چگونه Circular Dependency را پیدا کنیم؟

با ابزارهایی مانند madge، dpdm و dependency-cruiser.

آیا Barrel Export توصیه می‌شود؟

در سطح Feature بله، در سطح کل پروژه خیر، چون Tree Shaking را مختل می‌کند.

چگونه Tree Shaking را فعال کنیم؟

با ESM، Named Export، "sideEffects": false و پرهیز از Barrel Files سراسری.

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

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

چگونه Module Resolution را تنظیم کنیم؟

بسته به بستر: Bundler برای Frontend، NodeNext برای Node.js.

آیا Path Mapping بر عملکرد اثر دارد؟

Path Mapping فقط برای توسعه است. در Production، Bundler مسیرهای واقعی را Resolve می‌کند.

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

اشتباه علت راه‌حل
Circular Dependency عدم لایه‌بندی بازطراحی Dependency Graph
Barrel File سراسری سادگی Import Barrel در سطح Feature
Default Export سراسری عادت قدیمی Named Export
عدم استفاده از import type ناآشنایی import type برای Typeها
CommonJS در پروژه مدرن عدم مهاجرت ESM
عدم Tree Shaking عدم تنظیم sideEffects "sideEffects": false
Path Mapping ناسازگار عدم هماهنگی tsconfig و Bundler هماهنگی Pathها

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

در سطح معماری، استفاده از Module در TypeScript نیازمند یک استراتژی جامع است:

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

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

۳. Dependency Inversion: برای کاهش Coupling.

۴. Hexagonal Architecture: جداسازی Domain از Infrastructure.

۵. Code Splitting: با import() برای Lazy Loading.

۶. Bundle Analysis: با webpack-bundle-analyzer یا rollup-plugin-visualizer.

۷. Public API Pattern: تعریف مرزهای مشخص برای هر Feature.

۸. Automation: تحلیل خودکار Dependency Graph در CI/CD.

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

نتیجه

استفاده از Moduleها در TypeScript یک تصمیم معماری بنیادین است که بر نگهداشت‌پذیری، مقیاس‌پذیری، Tree Shaking و سرعت Build اثر می‌گذارد. طراحی صحیح Dependency Graph، پرهیز از Circular Dependency، استفاده از ESM و Named Export، و لایه‌بندی معماری، ستون‌های یک پروژه‌ی سالم هستند.

💡 اگر تجربه‌ای در معماری Module در پروژه‌های TypeScript داشته‌اید، برای ما جالب است بدانید کدام چالش بیشترین زمان را از تیم شما گرفت: Circular Dependency، Tree Shaking یا Barrel Files. تجربه‌ی خودتان را در دیدگاه‌ها بنویسید.