تقسیم کد TypeScript با import و export چطور انجام میشود؟
تحلیل مهندسی تقسیم کد TypeScript با import و export از منظر Module Boundary، Public API، Circular Dependency و Code Splitting؛ راهنمای عملی برای معماران نرمافزار.
تقسیم کد 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 ناموفق.
راهحلها
- استخراج Type مشترک: انتقال Type به Module سوم.
- Lazy Import: استفاده از
import(). - Dependency Injection: تزریق وابستگی.
- Event-based Communication: جایگزینی فراخوانی مستقیم.
- لایهبندی معماری: تعریف مرزهای واضح.
// راهحل با 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 تدریجی
- شناسایی Featureها: نقشهبرداری از قابلیتهای کسبوکار.
- تعریف Public API: مشخص کردن Exportهای هر Feature.
- استخراج کد مشترک: انتقال به
shared/. - انتقال تدریجی: هر Feature در یک PR.
- بهروزرسانی Importها: با Path Mapping.
- حذف کد قدیمی: پس از تأیید تست.
- پایش 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. تجربهی خودتان را در دیدگاهها بنویسید.