در یکی از پروژه‌های Node.js که توسط تیم دیگری شروع شده بود، یک خطای عجیب مرا دو روز زمین گیر کرد. 

کد کامپایل می‌شد، تست‌های ساده پاس می‌شدند، اما در runtime یک شی ناگهان undefined می‌شد. بعد از چند بار backtracking در کد، فهمیدم ریشه در یک circular dependency بین دو ماژول بود. 

 ماژول A در ابتدای فایل، ماژول B را import می‌کرد و ماژول B هم در ابتدای فایل، ماژول A را. TS و Node هر دو این ساختار را قبول می‌کنند، اما ترتیب اجرا باعث می‌شود که یکی از دو ماژول، در لحظه‌ی استفاده، نیمه‌ساخته باشد.

این اتفاق باعث شد، فهم من از ماژول‌ها در TypeScript  برای همیشه تغییر کند،

import و export فقط سینتکس نیستند؛ معماری جریان داده در پروژه هستند.


ماژول در TypeScript یکی از پایه‌ای‌ترین مفاهیم این زبان است که اغلب دست‌کم گرفته می‌شود. 

تفاوت بین یک پروژه‌ی TS با ساختار تمیز و یک پروژه‌ی به‌هم‌ریخته، اغلب در همان تصمیم‌های کوچک مربوط به import و export نهفته است. 


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



چرا ماژول‌ها در TypeScript این‌قدر مهم هستند؟

ماژول‌ها سه نقش هم‌زمان در پروژه ایفا می‌کنند که در بلندمدت، تفاوت را می‌سازند:

  • نقش مرزبندی: ماژول، مرز یکپارچگی کد است. هر فایل، یک واحد مستقل با مسئولیت مشخص است. اگر این مرزبندی درست طراحی شود، تغییرات در یک بخش پروژه، بخش‌های دیگر را نمی‌شکند. مطالعه‌ی این لایه در تفاوت تایپ اسکریپت و جاوااسکریپت آمده است.
  • نقش گراف وابستگی: import بین ماژول‌ها، یک گراف جهت‌دار می‌سازد. اگر این گراف پیچیده یا چرخه‌ای شود، پروژه به یک هزارتوی غیرقابل نگهداری تبدیل می‌شود. در پروژه‌های بزرگ، پایش این گراف یک فعالیت نگهداری مستمر است.
  • نقش کارایی: ساختار ماژول‌ها مستقیما روی tree-shaking، code splitting و حجم نهایی bundle اثر می‌گذارد. یک ماژول با exportهای بی‌رویه، مانع از حذف کدهای استفاده‌نشده می‌شود. مطالعه‌ی موازی این لایه در بهینه سازی جاوااسکریپت آمده است.

در پروژه‌ای که یک داشبورد تحلیلی بود، گراف وابستگی ماژول‌ها به بیش از ۳۰۰ گره رسیده بود و در نقاط مختلفی circular dependency داشت. نتیجه این  شد که ارگذاری اولیه سایت کند، tree-shaking بی‌اثر و هر تغییر کوچک، چندین باگ در جای دیگر ایجاد می‌کرد.

 بازطراحی ساختار ماژول‌ها در سه هفته انجام شد و حجم bundle را حدود ۴۰٪ کاهش داد. این یک سرمایه‌گذاری معماری است که در طول عمر پروژه، چندین برابر برمی‌گردد.

ماژول‌ها در TypeScript، بیشتر از یک ابزار فنی، یک زبان طراحی هستند: با آن‌ها به کامپایلر، به تیم و به خودتان می‌گویید هر تکه از کد چه مسئولیتی دارد و چه محدوده‌ای از پروژه را می‌پوشاند.


ماژول در JS و TS: شباهت و تفاوت

ماژول‌ها در TS تقریبا همان ساختار ماژول‌ها در JS مدرن (ES Modules) هستند، اما   چند تفاوت مهم وجود دارد:

ویژگیJavaScriptTypeScript
سینتکس import/exportES Modulesهمان ES Modules
Type-only importندارددارد (import type)
Namespaceندارددارد (میراث قدیمی)
Declaration Mergingندارددارد (برای interface)
Path Aliasندارددارد (از طریق tsconfig)
Compile-time بررسیندارددارد (ایمپورت‌های ناموجود خطا می‌دهند)


تفاوت بنیادی این است که در TS، کامپایلر می‌تواند در زمان توسعه خطاهای ایمپورت را تشخیص دهد. اگر مسیر یک ماژول اشتباه باشد، اگر یک export وجود نداشته باشد، یا اگر تایپ‌ها مطابقت نداشته باشند، کامپایلر به شما می‌گوید. این یک لایه‌ی محافظت اولیه است که در JS فقط در runtime قابل تشخیص است.

نکته‌ی ظریفی که وجود دارد این است که TS تمام مزایای ماژول‌ها در JS را به ارث می‌برد. 

یعنی چیزی که در مورد ساختار ماژول‌ها در ES Modules یاد می گیرید، مستقیما در TS هم معتبر است. اگر با ماژول‌ها در JS آشنا نیستید، ابتدا آموزش جاوااسکریپت از صفر را مرور کنید و بعد به این مقاله برگردید.


Export و انواع آن

TypeScript دو نوع export اصلی دارد که هرکدام کاربرد مشخصی دارند:

۱. Named Export

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

export function subtract(a: number, b: number): number {
  return a - b;
}

export const PI = 3.14159;

export class Calculator {
  // ...
}

Named Export ابزار اصلی من در پروژه‌هاست. هر چیزی که صادر می‌کنید، یک نام مشخص دارد و مصرف‌کننده باید همان نام را در import بنویسد. مزیت آن این است  کامپایلر می‌تواند دقیقا بفهمد چه چیزی استفاده شده و چه چیزی نه. این پایه‌ی tree-shaking است.

۲. Default Export

// User.ts
export default class User {
  constructor(public name: string, public email: string) {}
}

Default Export فقط یک مقدار در هر ماژول صادر می‌کند. مزیت ظاهری آن ایت است که سینتکس کوتاه‌تر در import می شود و  عیب آن این است که  کامپایلر نمی‌تواند به‌طور خودکار بفهمد که آیا استفاده می‌شود یا نه، و این روی tree-shaking اثر می‌گذارد.

۳. Re-export و Barrel Files

// index.ts
export { add, subtract } from "./math";
export { User } from "./User";
export type { Product } from "./Product";

الگوی Barrel (فایل index.ts در هر پوشه) در پروژه‌های متوسط و بزرگ پرکاربرد است. مزیتش این است که  امکان import کردن چند چیز از یک پوشه بدون نیاز به دانستن مسیر دقیق فایل‌ها. اما یک مشکل  دارد و آن این است  Barrel می‌تواند باعث بارگذاری ماژول‌های اضافه شود اگر درست مدیریت نشود.


انتخاب بین Named و Default

قاعده‌ی من در پروژه‌ها این است که  از Named Export استفاده کنید، مگر اینکه واقعاً دلیل قوی‌ای برای Default داشته باشید. دلایل این ترجیح:

  • قابلیت جستجو: پیدا کردن add  در کدبیس، نام export را نشان می‌دهد. اگر Default Export باشد، پیدا کردن بر اساس نام سخت‌تر است.
  • tree-shaking موثر: bundlerها می‌توانند Named Exportهای استفاده‌نشده را حذف کنند. با Default Export این کار سخت‌تر است.
  • جلوگیری از نام‌گذاری دلخواه: در Default Export، هر مصرف‌کننده می‌تواند نام دلخواه بدهد. این باعث ناهماهنگی در تیم می‌شود.
  • کامپایلر دقیق‌تر: با Named Export، TS راحت‌تر می‌تواند تشخیص دهد که آیا یک export استفاده می‌شود یا نه.

نقطه‌ی استثنایی که وجود دارد این است که  در فریم‌ورک‌های React و Vue، برای کامپوننت‌های اصلی معمولا Default Export استفاده می‌شود چون ابزارهای توسعه (مثل Fast Refresh) با آن بهتر کار می‌کنند. این یک توافق عملی است که در پروژه‌های فرانت‌اند رایج است. مطالعه‌ی بیشتر در تایپ اسکریپت با React.


Import و انواع آن

Import هم انواعی دارد که هرکدام در سناریوهای خاص کاربرد دارند:

۱. Named Import

import { add, subtract } from "./math";

ابزار اصلی. فقط چیزهایی که در export تعریف شده بودند را می‌آورد و کامپایلر دقیقاً می‌داند چه چیزی استفاده می‌شود.

۲. Namespace Import

import * as math from "./math";

math.add(1, 2);
math.subtract(5, 3);

همه‌ی exportهای یک ماژول را در یک شی  جمع می‌کند. کاربرد: در جاهایی که از چند export هم‌نام با ماژول‌های دیگر استفاده می‌کنید و می‌خواهید تفکیک کنید. در پروژه‌های معمولی کمتر استفاده می‌شود چون tree-shaking را سخت‌تر می‌کند.

۳. Default Import

import User from "./User";

فقط مقدار Default Export را می‌آورد. نام می‌تواند هر چیزی باشد چون در سمت exporter تعیین نشده است.

۴. Mixed Import

import User, { UserRole, validateUser } from "./User";

ترکیب Default و Named. الگویی که در پروژه‌های React زیاد می‌بینم اما توصیه می‌کنم تا حد ممکن از آن اجتناب کنید، چون خوانایی را در importهای طولانی کم می‌کند.

۵. Type-only Import

import type { User } from "./types";

let user: User;  // OK
new User();      // خطا! User یک تایپ است، نه یک کلاس

این نوع import در TypeScript منحصربه‌فرد است و در بخش بعدی به‌تفصیل به آن می‌رسیم.


type-only import و چرا حیاتی است

یکی از تفاوت‌های اساسی TS با JS، وجود import type است. این سینتکس به‌طور صریح به کامپایلر می‌گوید که این import، فقط برای تایپ‌ها استفاده می‌شود و در runtime نباید در خروجی JS باقی بماند:

import type { User, Product } from "./models";

interface Order {
  user: User;
  product: Product;
  total: number;
}

در این مثال، User و Product فقط برای تعریف تایپ استفاده شده‌اند. با import type، مطمئن می‌شوید که در خروجی JS نهایی، این import وجود ندارد و ماژول مبدأ در runtime بارگذاری نمی‌شود.


چرا این موضوع مهم است؟

  1. جلوگیری از خطاهای runtime: در بعضی سناریوها، ایمپورت کردن یک ماژول برای تایپ، می‌تواند باعث اجرای side effectهای آن ماژول شود. مثلاً اگر ماژولی در بارگذاری، یک singleton را initialize می‌کند.
  2. حجم کمتر در bundle: کامپایلر می‌تواند به‌طور مطمئن این importها را در زمان build حذف کند. در پروژه‌های بزرگ، این تفاوت محسوسی در حجم bundle می‌سازد.
  3. رفع Circular Dependency ناشی از تایپ: اگر فقط برای تایپ یک ماژول را import می‌کنید، با import type می‌توانید circular dependency را در runtime حذف کنید — حتی اگر گراف تایپ‌ها هنوز چرخه‌ای باشد.

قاعده‌ی من در پروژه‌ها این است که  هر import که فقط برای تایپ استفاده می‌شود، با import type نوشته می‌شود. در tsconfig، با فعال‌کردن verbatimModuleSyntax: true (یا در نسخه‌های قدیمی‌تر importsNotUsedAsValues)، کامپایلر را مجبور می‌کنید که تفاوت را به‌طور خودکار بررسی کند. مطالعه‌ی تنظیمات کامل در تنظیمات tsconfig قبلا بررسی شد.

در پروژه‌های بزرگ TypeScript، تفاوت بین import و import type می‌تواند تفاوت بین یک bundle بهینه و یک bundle بادکرده باشد. دو کلمه‌ی کوچک، اثر معماری بزرگی دارند.


CommonJS در برابر ES Modules

TypeScript دو سیستم ماژول اصلی را پشتیبانی می‌کند و درک تفاوت‌شان در پروژه‌های Node.js حیاتی است:

معیارCommonJSES Modules
سینتکسrequire و module.exportsimport و export
بارگذاریهمگامغیرهمگام (async)
Tree-shakingمحدودبهینه
پشتیبانی در Node.jsبومیاز نسخه ۱۴ به بعد
پسوند فایل.js.mjs یا با type: module
پشتیبانی در TSکاملکامل

در tsconfig، تنظیم module تعیین می‌کند که خروجی به کدام سیستم کامپایل شود:

  • "module": "CommonJS" برای پروژه‌های Node.js قدیمی یا کتابخانه‌هایی که با require مصرف می‌شوند.
  • "module": "ESNext" یا "ES2020" برای پروژه‌های مدرن که bundler استفاده می‌کنند.
  • "module": "NodeNext" برای پروژه‌های Node.js مدرن که می‌خواهند هم CommonJS و هم ESM را پشتیبانی کنند.

تجربه‌ی من در پروژه‌های Node.js به من نشان داد که بهتر است  از NodeNext استفاده کنید اگر Node ۱۶ یا بالاتر دارید. برای پروژه‌های قدیمی‌تر، CommonJS انتخاب پیش‌فرض است. مطالعه‌ی بیشتر در تایپ اسکریپت با Node.js در این مورد بیشتر به شما کمک خواهد کرد. 


Module Resolution در tsconfig

یکی از پرکاربردترین تنظیمات tsconfig که مستقیماً روی ساختار ماژول‌ها اثر می‌گذارد، moduleResolution است:

{
  "compilerOptions": {
    "moduleResolution": "node",
    "baseUrl": "./src",
    "paths": {
      "@components/*": ["components/*"],
      "@utils/*": ["utils/*"],
      "@types/*": ["types/*"]
    }
  }
}

با این تنظیمات، می‌توانید به‌جای مسیرهای نسبی طولانی مثل ../../../components/Button، از @components/Button استفاده کنید. این یک الگوی استاندارد در پروژه‌های متوسط و بزرگ است که خوانایی کد را به‌طور محسوسی افزایش می‌دهد.

مقادیر مختلف moduleResolution

  • "node" — الگوریتم قدیمی Node.js. سازگار با اکثر پروژه‌ها.
  • "node16" یا "nodenext" — الگوریتم جدید Node.js با پشتیبانی از ESM.
  • "bundler" — برای پروژه‌های مدرن که با Vite، esbuild یا Webpack build می‌شوند.
  • "classic" — الگوریتم قدیمی TS. تقریباً هیچ‌وقت استفاده نمی‌شود.

قاعده‌ی من در پروژه‌های جدید: "bundler" برای فرانت‌اند، "nodenext" برای Node.js. مطالعه‌ی بیشتر در تنظیمات tsconfig.


Dynamic Import و Code Splitting

در TypeScript، امکان import کردن یک ماژول به‌صورت پویا در runtime هم وجود دارد:

async function loadModule() {
  const { calculateTax } = await import("./tax");
  return calculateTax(100);
}

Dynamic Import کاربردهای مهمی در پروژه‌های مدرن دارد:

  1. Code Splitting: bundlerها می‌توانند این importها را به chunkهای جداگانه تبدیل کنند که فقط در زمان نیاز دانلود می‌شوند. الگوی اصلی lazy-loading در React و Vue.
  2. بارگذاری شرطی: اگر یک قابلیت فقط برای بخش کوچکی از کاربران لازم است، می‌توانید در زمان استفاده بارگذاری کنید.
  3. Pollyfill و fallback: در بعضی سناریوها، ماژول مورد نیاز به پلتفرم وابسته است و باید در runtime انتخاب شود.

در یک پروژه‌ی React، با Dynamic Import، کامپوننت‌های سنگین مثل ویرایشگر متن یا نمودارها را به chunkهای جداگانه منتقل کردیم. نتیجه این شد که حجم اولیه‌ی صفحه حدود ۳۵٪ کاهش پیدا کرد و بارگذاری فقط در زمان نیاز انجام شد. مطالعه‌ی موازی در تایپ اسکریپت با React و بهینه سازی جاوااسکریپت در فهم بیشتر این مطلب به شما کمک خواهد کرد. 


Namespace و جایگاه فراموش‌شده‌اش

TypeScript یک ساختار قدیمی به‌نام namespace هم دارد که قبل از ES Modules معرفی شد:

namespace MathUtils {
  export function add(a: number, b: number): number {
    return a + b;
  }
  export function multiply(a: number, b: number): number {
    return a * b;
  }
}

MathUtils.add(1, 2);

در سال‌های اخیر، استفاده از namespace در پروژه‌های جدید توصیه نمی‌شود. دلایل:

  • عدم سازگاری با ES Modules: namespace توسط bundlerهای مدرن به‌طور بهینه پردازش نمی‌شود.
  • tree-shaking ضعیف: namespace در خروجی JS یک IIFE می‌سازد که نمی‌تواند به‌طور کامل حذف شود.
  • جایگزین‌های بهتر: یک ماژول با Named Exportها همان کار را با بهره‌وری بالاتر انجام می‌دهد.

تنها کاربرد باقی‌مانده‌ی namespace در پروژه‌های مدرن، گسترش تایپ‌های کتابخانه‌های جهانی است:

declare namespace Express {
  interface Request {
    user?: User;
  }
}

این الگو در پروژه‌های Node.js با Express برای اضافه‌کردن فیلد به Request استفاده می‌شود. در تمام موارد دیگر، به‌جای namespace از ماژول‌های معمولی استفاده کنید. مطالعه‌ی بیشتر در اینترفیس در تایپ اسکریپت.


Circular Dependency: قاتل خاموش ماژول‌ها

Circular Dependency یعنی ماژول A به B وابسته باشد و B به A. در پروژه‌های بزرگ، این مشکل به‌طور طبیعی رخ می‌دهد اگر مرزهای ماژول‌ها درست تعریف نشده باشند:

// a.ts
import { bFunction } from "./b";
export function aFunction() {
  return bFunction() + " from a";
}

// b.ts
import { aFunction } from "./a";
export function bFunction() {
  return aFunction() + " from b";
}

هر دو ماژول به هم وابسته‌اند. TS و Node این ساختار را قبول می‌کنند اما در runtime، بسته به ترتیب بارگذاری، ممکن است یکی از توابع undefined باشد و خطای «is not a function» رخ دهد.


راه‌حل‌های عملی

  1. استخراج منطق مشترک به ماژول سوم: اگر دو ماژول به هم وابسته‌اند، احتمالاً منطق مشترکی دارند که باید به یک ماژول سوم منتقل شود.
  2. استفاده از import type: اگر فقط تایپ‌ها به هم وابسته‌اند، با import type مشکل runtime حل می‌شود.
  3. Dynamic import: در سناریوهایی که وابستگی در سطح معماری ناگزیر است، dynamic import می‌تواند چرخه را در runtime بشکند.
  4. بازطراحی مرزها: اگر یک circular dependency در چندین نقطه تکرار شود، احتمالاً مرزهای ماژول‌های شما نیاز به بازطراحی دارند.

در یک پروژه‌ی Node.js، در یک پرونده‌ی runtime، خطای عجیبی به‌وجود آمد که در debug عادی پیدا نشد. با ابزار madge، گراف وابستگی ماژول‌ها را رسم کردیم و سه circular dependency پیدا شد که همه‌شان از شباهت نام‌گذاری دو ماژول ناشی می‌شدند. بعد از استخراج منطق مشترک به یک ماژول سوم، مشکل کامل حل شد. مطالعه‌ی موازی در ابزارهای CI/CD و گیت در وردپرس در این مورد کارساز خواهد بود.


الگوهای ساختاری در پروژه‌های واقعی

سه الگوی ساختاری که در پروژه‌های خودم بیشترین استفاده را داشته‌اند:

  1. Barrel Files با احتیاط: در هر پوشه، یک فایل index.ts که exportهای آن پوشه را دوباره صادر می‌کند.که باعث  importهای کوتاه‌تر می شود.  اما  اگر Barrel در پوشه‌های زیاد تکرار شود، ممکن است bundler مجبور به بارگذاری ماژول‌های اضافه شود. راه‌حل  می تواند این باشد که  Barrelها را در سطح پوشه‌های بالایی نگه دارید، نه در هر پوشه‌ی کوچک.
  2. Domain-first Structure: ساختار پروژه بر اساس دامنه، نه بر اساس نوع فایل. مثلاً پوشه‌ی users/ شامل همه‌ی ماژول‌های مربوط به کاربر (types، services، components) در یک جا. این ساختار در پروژه‌های بزرگ، نگهداری را ساده‌تر می‌کند چون تغییرات مربوط به یک دامنه در یک جا متمرکز است. مطالعه‌ی  بیشتر  در توسعه وردپرس از صفر.
  3. Shared Types در ماژول جدا: تمام تایپ‌های مشترک (User، Product، Order) در یک ماژول جدا با import type صادر می‌شوند. این الگو در پروژه‌های فول‌استک که frontend و backend در یک monorepo هستند، تفاوت محسوسی در سرعت توسعه می‌سازد.


اشتباهاتی که در پروژه‌ها دیدم

  • Circular Dependency پنهان: گاهی این چرخه‌ها فقط در عمق چند سطحی ظاهر می‌شوند. بهتر است از  ابزارهایی مثل madge و dependency-cruiser را در CI قرار دهید و پایش کنید.
  • Default Export در همه‌جا: در پروژه‌ای، همه‌ی ماژول‌ها Default Export داشتند و همین باعث شد که tree-shaking تقریباً بی‌اثر باشد. مهاجرت به Named Export، حجم bundle را حدود ۲۰٪ کاهش داد.
  • نبود import type: در پروژه‌ای که ماژول‌های تایپ و runtime جدا نبودند، کامپایلر مجبور بود تمام تایپ‌ها را در bundle نهایی نگه دارد. اضافه کردن import type در حدود ۵۰ نقطه، حجم JS نهایی را کاهش داد.
  • Barrel File در هر پوشه: در پروژه‌ای که در هر پوشه یک index.ts بود، بارگذاری اولیه سایت به دلیل بارگذاری تمام ماژول‌های پوشه‌ها، حدود ۲۰۰ میلی‌ثانیه بیشتر طول می‌کشید. کاهش Barrelها، این مشکل را حل کرد.
  • مخلوط کردن CommonJS و ES Modules: در پروژه‌ای که نیمی از ماژول‌ها با require و نیمی با import نوشته شده بودند، رفتار در runtime غیرقابل پیش‌بینی بود. همیشه یک سیستم ماژول را انتخاب کنید.
  • Module Alias بی‌قاعده: در پروژه‌ای، بیش از ده Alias مختلف تعریف شده بود که نگهداری‌شان دشوار بود. قاعده‌ی من: حداکثر سه یا چهار Alias اصلی.
  • نبود مرز مشخص بین لایه‌ها: در پروژه‌ای که ماژول‌های دامنه، مستقیم از ماژول‌های infrastructure import می‌کردند، تست‌های واحد غیرممکن شده بود. مرز ماژول‌ها باید با مرزهای معماری هماهنگ باشد.
  • استفاده از namespace در کد جدید: همان‌طور که پیش‌تر گفتم، namespace در پروژه‌های مدرن توصیه نمی‌شود. این یک عادت قدیمی است که باید با ماژول جایگزین شود.

بخشی از این اشتباهات در خطاهای رایج تایپ اسکریپت و اشتباهات رایج توسعه هم آمده است.


لایه‌ای پایین‌تر از سینتکس import

اینجا وارد لایه‌ای می‌شوم که در پروژه‌های معمولی به آن نگاه نمی‌شود اما برای مهندسان پلتفرم و توسعه‌دهنده‌های ارشد اهمیت دارد. آنچه کامپایلر و bundler با ماژول‌های شما می‌کنند، در پنج مفهوم خلاصه می‌شود:

  1. Module Evaluation Order و Hoisting: ES Modules از یک الگوریتم دقیق برای بارگذاری و ارزیابی استفاده می‌کنند. فاز اول، تمام ماژول‌ها parse و وابستگی‌ها resolve می‌شوند. فاز دوم، ابتدا همه‌ی importها اجرا می‌شوند و بعد بدنه‌ی ماژول. این ترتیب دو مرحله‌ای، تفاوت بنیادی با CommonJS دارد که بدنه‌ی ماژول را بلافاصله اجرا می‌کند. این تفاوت توضیح می‌دهد چرا بعضی رفتارها در ESM و CommonJS متفاوت است. مطالعه‌ی موازی در مفاهیم پیشرفته جاوااسکریپت و آموزش جاوااسکریپت از صفر.
  2. Live Bindings و ثابت‌های Imported: در ES Modules، وقتی یک مقدار از یک ماژول import می‌کنید، در واقع یک «binding زنده» به آن مقدار دارید. یعنی اگر ماژول مبدأ مقدار را تغییر دهد، در ماژول مصرف‌کننده هم منعکس می‌شود. این ویژگی ظاهراً ساده، پیامدهای عملی زیادی دارد  مثلاً اگر یک متغیر در ماژول مبدأ دوباره تخصیص شود، ماژول مصرف‌کننده مقدار جدید را می‌بیند. مطالعه‌ی  بیشتر در ماژول ها در تایپ اسکریپت.
  3. Tree Shaking و Dead Code Elimination: bundlerهایی مثل Vite و Webpack با تحلیل static، ماژول‌ها و exportهایی که استفاده نمی‌شوند را حذف می‌کنند. اما این تحلیل با Default Export و Side Effects دشوارتر می‌شود. برای اینکه tree-shaking مؤثر باشد، از Named Export استفاده کنید، Side Effectها را در ماژول‌های جدا نگه دارید، و sideEffects: false را در package.json علامت‌گذاری کنید اگر مطمئن هستید که ماژول‌های شما بدون side effect هستند. مطالعه‌ی موازی این لایه در بهینه سازی جاوااسکریپت و بهینه‌سازی سرعت سایت آمده است.
  4. Module Resolution Algorithm: کامپایلر TypeScript و bundlerها الگوریتم‌های دقیقی برای پیدا کردن ماژول‌ها دارند. تفاوت node، node16 و bundler در نحوه‌ی پیدا کردن extensionها، index files و package.json exports است. Understanding این الگوریتم در پروژه‌های monorepo و کتابخانه‌های عمومی حیاتی است. مطالعه‌ی بیشتر در تنظیمات tsconfig و تایپ اسکریپت با Node.js.
  5. Type-Only Erasure و Interaction با Bundlers: TypeScript پس از کامپایل، اطلاعات تایپ‌ها را کاملاً حذف می‌کند. اما bundlerهایی مثل esbuild و SWC که تایپ‌چکینگ را نادیده می‌گیرند، ممکن است بعضی import typeها را اشتباه پردازش کنند. برای اطمینان از رفتار درست، از verbatimModuleSyntax: true در tsconfig استفاده کنید. این تنظیم کامپایلر را مجبور می‌کند که تفاوت import و import type را دقیق رعایت کند. مطالعه‌ی بیشتر در ابزارهای CI/CD و تایپ اسکریپت با React.

یک تجربه‌ی واقعی از پروژه‌ای که با Live Bindings روبرو شدیم این بود که در یک ماژول تنظیمات، یک متغیر let export کرده بودیم که در زمان اجرا توسط یک ماژول دیگر تغییر می‌کرد.

 در تست‌های جداگانه، همه‌چیز درست بود، اما در اجرای واقعی، مقدار مشاهده‌شده توسط ماژول‌های مصرف‌کننده گاهی متفاوت از انتظار بود. ریشه این مشکل از اینجا ناشی می شد که  Live Binding باعث می‌شد که مصرف‌کننده‌ها همیشه آخرین مقدار را ببینند، نه مقدار لحظه‌ی import. راه‌حل این مشکل ،  استفاده از یک تابع getter به‌جای export مستقیم متغیر، که رفتار را صریح و قابل پیش‌بینی می‌کرد.

اگر روی پروژه‌های وردپرسی در حال کار هستید و می‌خواهید این لایه‌ها را در development pipeline خود اعمال کنید، پیشنهاد می‌کنم ابتدا به توسعه وردپرس از صفر نگاهی بیندازید.

برای مطالعه‌ی موازی با استانداردها و معماری، استانداردهای HTML و CSS و CSS مدرن از Flexbox تا Grid دید وسیع‌تری می‌دهند.

 برای درک این لایه در چارچوب کارایی، بهینه سازی جاوااسکریپت و بهینه‌سازی سرعت سایت منابع کلیدی هستند. اگر روی موضوع کیفیت کد و تست متمرکز هستید، گیت در وردپرس و ابزارهای CI/CD را هم ببینید.

ماژول‌ها در TypeScript مثل ستون‌های یک ساختمان هستند: اگر مرزها و وزن هرکدام درست تعریف شود، ساختمان می‌ایستد؛ اگر نه، حتی زیباترین نما هم در برابر اولین تغییر فرو می‌ریزد.


ماژول‌ها در TypeScript را می‌توان در یک جمله خلاصه کرد: «ابزاری برای تعریف مرزها، گراف وابستگی و جریان داده در پروژه.» سه درس که از این مسیر با خودم بردم:

  1. Named Export را به Default ترجیح دهید. جز در موارد خاص مثل کامپوننت‌های React، از Named Export استفاده کنید. این یک تصمیم کوچک است که در بلندمدت، tree-shaking، جستجوپذیری و خوانایی را به‌طور محسوسی بهبود می‌دهد.
  2. import type را جدی بگیرید. جدا کردن تایپ‌ها از کد runtime، تفاوت بین یک bundle بهینه و یک bundle بادکرده است. با verbatimModuleSyntax در tsconfig، این تفکیک را به یک قاعده‌ی کامپایلری تبدیل کنید.
  3. گراف وابستگی را پایش کنید. در پروژه‌های بزرگ، Circular Dependency یک قاتل خاموش است. با ابزارهایی مثل madge، این گراف را در CI پایش کنید و مرزهای ماژول‌ها را در برابر پیچیدگی محافظت کنید.


مسیر یادگیری فرانت‌اند با این نوشته تمام نمی‌شود. اگر می‌خواهید مرحله‌ی بعدی را بردارید، تنظیمات tsconfig، تایپ اسکریپت با React و تایپ اسکریپت با Node.js سه قدم منطقی بعدی هستند.

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

اگر هم به سمت خطاها و دیباگ می‌روید، خطاهای رایج تایپ اسکریپت و ابزارهای CI/CD منابع کلیدی هستند.

ماژول‌ها همیشه یکی از آن موضوعاتی بوده‌اند که در تجربه‌ی خودم، بازبینی‌شان بعد از یک سال، ارزشمندتر از روز اول بوده. 


اگر شما هم پروژه‌ای را از یک ساختار ماژول به‌هم‌ریخته به یک ساختار تمیز برده‌اید یا برعکس، یک بار به دام Circular Dependency افتاده‌اید و روش خاصی برای کشفش پیدا کرده‌اید، آن تجربه را برای ما توضیح دهید. 

این توضیحات ، برای کسی که امروز در حال طراحی ساختار یک پروژه‌ی تازه است، از هر مستند رسمی ارزش عملی بیشتری دارند.