ماژولها در TypeScript چگونه کار میکنند و چگونه از آنها استفاده کنیم؟
تحلیل مهندسی Module System در TypeScript از منظر Resolution Algorithm، Module Graph، ESM vs CommonJS و Interop؛ راهنمای عمیق برای مهندسان ارشد.
ماژولها در 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"، الگوریتم بهترتیب زیر عمل میکند:
- بررسی
./utils/bar.ts - بررسی
./utils/bar.tsx - بررسی
./utils/bar.d.ts - بررسی
./utils/bar/index.ts - بررسی
package.jsonدر./utils/bar/ - جستجو در
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 است:
- استفاده از ESM: Static Analysis ممکن میشود.
- Named Export: Bundler میتواند Exportهای استفادهنشده را حذف کند.
- Side-effect-free: تعریف
"sideEffects": falseدرpackage.json. - Pure Functions: با
/*#__PURE__*/در صورت لزوم. - پرهیز از 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. تجربهی خودتان را در دیدگاهها بنویسید.