استفاده از Moduleها در TypeScript چطور پروژه را منظم میکند؟
تحلیل معماری Module در TypeScript از منظر Resolution، Bundling، Tree Shaking و طراحی Dependency Graph؛ راهنمای مهندسی برای پروژههای مقیاسپذیر.
استفاده از 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
- استخراج Type مشترک: انتقال Type به Module سوم.
- Lazy Import: استفاده از
import(). - Dependency Injection: تزریق وابستگی بهجای Import مستقیم.
- Event-based Communication: جایگزینی فراخوانی مستقیم با Event.
- لایهبندی معماری: تعریف مرزهای واضح.
Tree Shaking و بهینهسازی Bundle
Tree Shaking فرآیند حذف کدهای استفادهنشده در زمان Bundling است. برای فعال بودن Tree Shaking:
- استفاده از ESM: CommonJS Tree Shaking را محدود میکند.
- Named Export: Default Export Tree Shaking را سختتر میکند.
- Side-effect-free: تعریف
"sideEffects": falseدرpackage.json. - Pure Functions: استفاده از
/*#__PURE__*/در صورت لزوم. - عدم استفاده از 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. تجربهی خودتان را در دیدگاهها بنویسید.