تقسیم کد TypeScript با import و export یکی از مهارت‌های بنیادین مهندسان ارشد است که مستقیماً بر نگهداشت‌پذیری، تست‌پذیری، Tree Shaking و مقیاس‌پذیری پروژه اثر می‌گذارد. تقسیم کد، فراتر از جداسازی فایل‌ها است؛ یک تصمیم معماری است که مرزهای مسئولیت (Module Boundary)، جهت وابستگی (Dependency Direction) و نقاط اتصال (Public API) را تعریف می‌کند. در TypeScript، ابزار اصلی این تقسیم، import و export است که امکان تعریف Scope اختصاصی، وابستگی صریح و Encapsulation را فراهم می‌کند. اما استفاده‌ی نادرست از این ابزار، می‌تواند به Circular Dependency، Barrel Files سراسری، Coupling بالا و Tree Shaking ناموفق منجر شود. در این راهنما، فرآیند مهندسی تقسیم کد TypeScript با import و export، از الگوها تا پیاده‌سازی عملی، بررسی می‌شود.

در یکی از پروژه‌های سازمانی، فایلی با ۴۰۰۰ خط کد شامل تمام منطق فروشگاه بود. پس از تقسیم به Moduleهای Feature-based، زمان Build ۳۵٪ کاهش یافت، تست‌نویسی ۳ برابر سریع‌تر شد و Circular Dependencyها از ۱۲ به صفر رسید. این تجربه نشان می‌دهد که تقسیم کد، یک سرمایه‌گذاری مهندسی است.

چرا کد را تقسیم کنیم

تقسیم کد با import و export مزایای زیر را فراهم می‌کند:

  • Scope Isolation: هر Module Scope اختصاصی دارد.
  • Explicit Dependencies: وابستگی‌ها در یک نگاه مشخص می‌شوند.
  • Encapsulation: پیاده‌سازی داخلی پنهان می‌ماند.
  • Tree Shaking: حذف کدهای استفاده‌نشده.
  • Testability: تست Moduleهای کوچک ساده‌تر است.
  • Parallel Development: تیم‌ها موازی کار می‌کنند.
  • Code Splitting: Lazy Loading برای بهینه‌سازی.
  • Reduced Cognitive Load: درک پروژه ساده‌تر.

Import و Export: ابزارهای اصلی

TypeScript دو مکانیزم اصلی برای تقسیم کد دارد:

Export

// utils/math.ts

// Named Export
export function add(a: number, b: number): number {
  return a + b;
}

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

// Re-export
export { divide } from "./divide";

// Default Export
export default function subtract(a: number, b: number): number {
  return a - b;
}

// Type Export
export type MathOperation = (a: number, b: number) => number;

// Const Export
export const PI = 3.14159;

Import

// main.ts

// Named Import
import { add, multiply } from "./utils/math";

// Default Import
import subtract from "./utils/math";

// Namespace Import
import * as Math from "./utils/math";
Math.add(1, 2);

// Aliased Import
import { add as sum } from "./utils/math";

// Type Import
import type { MathOperation } from "./utils/math";

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

Named Export در مقابل Default Export

ویژگی Named Export Default Export
Tree Shaking بهتر ضعیف‌تر
Refactoring ساده‌تر (IDE Support) دشوارتر
Name Collision قابل مدیریت عدم کنترل
Import Syntax صریح مبهم
Discoverability بالا پایین
Re-export ساده پیچیده‌تر

توصیه: استفاده از Named Export در پروژه‌های بزرگ، به‌ویژه برای Utilityها و Serviceها.

Public API و Module Boundary

هر Feature باید یک Public API مشخص داشته باشد:

src/
├── features/
│   ├── auth/
│   │   ├── components/
│   │   │   ├── LoginForm.tsx
│   │   │   └── RegisterForm.tsx
│   │   ├── hooks/
│   │   │   └── useAuth.ts
│   │   ├── services/
│   │   │   └── authService.ts
│   │   ├── types/
│   │   │   └── index.ts
│   │   └── index.ts    ← Public API
│   └── products/
└── shared/
    └── ui/
        └── index.ts
// 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";

قواعد Module Boundary:

  • دسترسی از خارج Feature تنها از طریق index.ts.
  • پیاده‌سازی داخلی (components، hooks، services) از خارج پنهان است.
  • هر Feature می‌تواند به shared/ وابسته باشد.
  • Featureها نباید به پیاده‌سازی داخلی یکدیگر وابسته باشند.

Barrel Files: مزایا و معایب

Barrel File فایلی است که Exportها را از چند فایل جمع‌آوری می‌کند:

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

مزایا

  • Importهای کوتاه‌تر.
  • کپسوله‌سازی ساختار داخلی.
  • Public API واضح.

معایب

  • Tree Shaking را مختل می‌کند.
  • Circular Dependency ایجاد می‌کند.
  • Build Time را افزایش می‌دهد.
  • Bundle Size را بزرگ می‌کند.

توصیه: Barrel File در سطح Feature استفاده شود، نه در سطح Root پروژه.

Circular Dependency و راه‌حل‌ها

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

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

پیامدها

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

راه‌حل‌ها

  1. استخراج Type مشترک: انتقال Type به Module سوم.
  2. Lazy Import: استفاده از import().
  3. Dependency Injection: تزریق وابستگی.
  4. Event-based Communication: جایگزینی فراخوانی مستقیم.
  5. لایه‌بندی معماری: تعریف مرزهای واضح.
// راه‌حل با Lazy Import
// a.ts
export const a = async () => {
  const { b } = await import("./b");
  return b();
};

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

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.

Dynamic Import و Code Splitting

// Static Import
import { HeavyComponent } from "./HeavyComponent";

// Dynamic Import
const HeavyComponent = lazy(() => import("./HeavyComponent"));

// Conditional Import
async function loadFeature() {
  if (featureEnabled) {
    const { feature } = await import("./feature");
    return feature;
  }
}

// In React Router
const ProductPage = lazy(() => import("./pages/ProductPage"));

مزایای Code Splitting:

  • کاهش Initial Bundle Size.
  • Improve First Contentful Paint (FCP).
  • Lazy Loading برای Routeها.
  • On-Demand Loading برای Featureها.

Refactoring تدریجی

  1. شناسایی Featureها: نقشه‌برداری از قابلیت‌های کسب‌وکار.
  2. تعریف Public API: مشخص کردن Exportهای هر Feature.
  3. استخراج کد مشترک: انتقال به shared/.
  4. انتقال تدریجی: هر Feature در یک PR.
  5. به‌روزرسانی Importها: با Path Mapping.
  6. حذف کد قدیمی: پس از تأیید تست.
  7. پایش Dependency Graph: پس از هر مرحله.

ابزارهای تحلیل Dependency

ابزار کاربرد خروجی
madge تحلیل Circular Graph، Text
dependency-cruiser Enforce قواعد Graph، Rules
dpdm Circular Detection Text
ts-unused-exports Exportهای استفاده‌نشده Text
knip کد و وابستگی بی‌استفاده Text
# Enforce rules با dependency-cruiser
module.exports = {
  forbidden: [
    {
      name: "no-circular",
      severity: "error",
      from: {},
      to: { circular: true },
    },
    {
      name: "shared-not-to-features",
      severity: "error",
      from: { path: "^src/shared" },
      to: { path: "^src/features" },
    },
  ],
};

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

Named Export بهتر است یا Default؟

Named Export برای Tree Shaking و Refactoring بهتر است. Default Export برای Componentهای Single Export مناسب است.

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

در سطح Feature بله، در سطح Root پروژه خیر.

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

با madge، dpdm یا dependency-cruiser.

آیا import type بر Bundle اثر دارد؟

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

چگونه Code Splitting را پیاده کنیم؟

با Dynamic Import و React.lazy یا Route-based Splitting.

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

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

چگونه Public API یک Feature را تعریف کنیم؟

با فایل index.ts در ریشه‌ی Feature و Export تنها از طریق آن.

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

اشتباه علت راه‌حل
Circular Dependency عدم لایه‌بندی بازطراحی Dependency Graph
Barrel File سراسری سادگی Import Barrel در سطح Feature
Default Export سراسری عادت قدیمی Named Export
عدم استفاده از import type ناآشنایی Type-only Import
Import از پیاده‌سازی داخلی Feature عدم احترام به Boundary Public API
عدم Code Splitting Bundler Configuration نادرست Dynamic Import
Import مستقیم با مسیر طولانی عدم Path Mapping tsconfig paths

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

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

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

۲. Dependency Inversion: وابستگی به Abstraction.

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

۴. Project References: در Monorepo.

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

۶. Automated Analysis: در CI/CD.

۷. Code Ownership: با CODEOWNERS.

۸. Progressive Migration: مهاجرت تدریجی.

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

نتیجه

تقسیم کد TypeScript با import و export یک تصمیم معماری است که با تعریف Module Boundary، Public API و جهت وابستگی شکل می‌گیرد. انتخاب Named Export، استفاده‌ی کنترل‌شده از Barrel Files، پرهیز از Circular Dependency و استفاده از Type-only Import، ستون‌های یک معماری سالم هستند. Code Splitting و Tree Shaking، بهره‌وری Bundle را افزایش می‌دهند.

💡 اگر تجربه‌ای در تقسیم کد TypeScript داشته‌اید، برای ما جالب است بدانیم کدام چالش بیشترین زمان را از تیم شما گرفت: Circular Dependency، Tree Shaking یا Public API. تجربه‌ی خودتان را در دیدگاه‌ها بنویسید.