TypeScript در عمل؛ strict، Runtime و مهاجرت امن

کامپایلر سبز است، اما پاسخ درگاه پرداخت چنین می‌رسد: { "amountRial": "2500000" }. تیم در کد نوشته بود amountRial: number و با یک Type assertion پاسخ را «قابل اعتماد» اعلام کرده بود. برنامه در Production رشته را با عدد جمع می‌کند و خطای مالی می‌سازد. TypeScript شکست نخورده است؛ قراردادی را بررسی کرده که خود تیم بدون شاهد به آن داده بود.

TypeScript وقتی ارزش می‌سازد که Typeها مدل یک واقعیت معتبر باشند، مرزهای Runtime اعتبارسنجی شوند و خطای Type در CI متوقف‌کننده باشد. افزودن پسوند .ts، نصب VS Code یا زیادکردن Interfaceها به‌تنهایی نرم‌افزار را امن، سریع یا مقیاس‌پذیر نمی‌کند.

این راهنما از تعریف TypeScript تا تصمیم پذیرش، طراحی Type، تنظیم tsconfig، اعتبارسنجی Runtime، مهاجرت JavaScript، Build، Test و سنجش نتیجه را پوشش می‌دهد. برای الگوهای سطح معماری و Design pattern به‌ترتیب به راهنمای معماری تمیز و راهنمای الگوهای طراحی وب مراجعه کنید؛ این صفحه مالک تصمیم و اجرای خود TypeScript است.

TypeScript چیست؟ یک Type checker برای Runtime جاوااسکریپت

TypeScript زبانی است که JavaScript را با Syntax مربوط به Type و یک تحلیل‌گر ایستا گسترش می‌دهد. تحلیل ایستا پیش از اجرا روابطی مانند ورودی و خروجی تابع، وجود Property، حالت‌های Union و سازگاری قراردادها را بررسی می‌کند. خروجی معمولاً JavaScript است و Type annotationها حذف می‌شوند.

طبق راهنمای رسمی TypeScript، یکی از اصول آن حفظ رفتار Runtime جاوااسکریپت است: Typeها پس از بررسی پاک می‌شوند و بر نحوه اجرای برنامه اثر مستقیم ندارند. نتیجه عملی:

  • TypeScript می‌تواند بسیاری از ناسازگاری‌ها را قبل از اجرا پیدا کند، نه همه Bugها را.
  • Type اشتباه، any یا Assertion می‌تواند اطمینان کاذب بسازد.
  • دانستن JavaScript، Event loop، Promise، Module و Runtime همچنان ضروری است.
  • Performance زمان اجرا عمدتاً تابع JavaScript خروجی، الگوریتم، DOM، Network و Runtime است؛ نه وجود Type annotation.

آیا مرورگر یا Node.js فایل TypeScript را مستقیم اجرا می‌کند؟

پاسخ یک «بله/خیر» ساده نیست. مرورگر عمومی را نباید مصرف‌کننده مستقیم همه Syntaxهای TypeScript فرض کرد؛ معمولاً Build tool یا Compiler خروجی JavaScript می‌سازد. در Node.js جدید، Type stripping داخلی می‌تواند فایل‌هایی را که فقط Syntax قابل‌حذف دارند اجرا کند. اما مستند رسمی Node.js تصریح می‌کند که این مسیر Type check انجام نمی‌دهد، tsconfig.json را نادیده می‌گیرد و قابلیت‌هایی که Transform لازم دارند محدودند.

Snapshot نسخه در ۱۲ اوت ۲۰۲۶

TypeScript ۷.۰ در ژوئیه ۲۰۲۶ با Compiler و Language server بومی مبتنی بر Go منتشر شد. اعلام رسمی TypeScript ۷ آن را برای استفاده Production آماده می‌داند، اما یک مرز مهم دارد: نسخه ۷.۰ هنوز API برنامه‌نویسی پایدار ارائه نمی‌کند. ابزارهایی که Compiler API را Embed می‌کنند یا زبان‌های درون‌گذاشته مانند بعضی جریان‌های Vue، Svelte، Astro، MDX و Angular ممکن است همچنان به TypeScript ۶ نیاز داشته باشند.

بنابراین «همه همین امروز مهاجرت کنند» توصیه معتبری نیست. Dependencyها، Pluginهای Editor، Linter، Framework checker، Declaration emit و Pipeline واقعی خود را در Branch آزمایشی بسنجید. TypeScript ۶ و ۷ قابلیت اجرای کنار هم دارند؛ معیار پذیرش، سازگاری و نتیجه پروژه است، نه شماره نسخه.

آیا پروژه شما به TypeScript نیاز دارد؟

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

Decision contract

پرسشنشانه ارزش بالاتر TypeScriptشاهد
کد چقدر تغییر می‌کند؟Refactor و توسعه هم‌زمان چند ماژولFiles per change، Regression و Lead time
چند قرارداد مشترک داریم؟API، Event، SDK، Package و State پیچیدهخطاهای Contract و Integration
چند نفر کد را لمس می‌کنند؟تیم چندنفره، Onboarding یا Ownership توزیع‌شدهReview time و سؤال‌های تکراری
پیامد خطا چیست؟پرداخت، دسترسی، سفارش یا داده حساسSeverity رخداد و هزینه بازیابی
اکوسیستم آماده است؟Type declaration معتبر و Toolchain سازگارPoC روی Package/Framework واقعی
تیم توان نگهداری دارد؟مالک Config، Upgrade و Type debt وجود داردCapacity و SLA رفع خطای Compiler

چه زمانی JavaScript ساده‌تر است؟

برای Script کوتاه و کم‌خطر، Prototype دورریختنی یا اتوماسیونی با مالک واحد و سطح تغییر کم، JavaScript همراه Test و JSDoc ممکن است هزینه کمتر و ارزش کافی داشته باشد. حتی می‌توانید با // @ts-check یا checkJs بخشی از تحلیل TypeScript را بدون تغییر پسوند دریافت کنید.

این تصمیم باید قابل بازبینی باشد. Trigger مهاجرت می‌تواند افزایش مشارکت‌کننده، رشد Contractهای خارجی، تکرار خطاهای Shape/Null، ساخت Package عمومی یا دشوارشدن Refactor باشد.

TypeScript چه چیزی را تضمین نمی‌کند؟

  • صحت منطق کسب‌وکار، محاسبه قیمت یا ترتیب عملیات؛
  • اعتبار داده API، فرم، Queue، Database یا Environment در Runtime؛
  • امنیت در برابر XSS، Injection، Broken authorization یا Dependency مخرب؛
  • Performance، مقیاس‌پذیری، Availability یا معماری تمیز؛
  • درست‌بودن Type declaration یک Package ثالث؛
  • نبود Race condition، Memory leak یا خطای Async.

Typeها را برای حالت‌های معتبر طراحی کنید

هدف نوشتن بیشترین Type نیست؛ هدف کاهش حالت‌های مبهم و ساختن قرارداد قابل تغییر است. از Inference شروع کنید و فقط در Boundary، API عمومی، State پیچیده و نقاطی که Intent روشن نیست Annotation اضافه کنید.

از Primitiveهای مبهم به Domain type بروید

تابعی با سه پارامتر string نمی‌گوید کدام رشته شماره سفارش، کدام شناسه کاربر و کدام کد رهگیری است. برای جلوگیری از جابه‌جایی تصادفی، می‌توان Type متمایز ساخت؛ اما Validation همچنان باید هنگام ایجاد مقدار انجام شود.

type OrderId = string & { readonly __brand: "OrderId" };
type UserId = string & { readonly __brand: "UserId" };

function asOrderId(value: string): OrderId {
  if (!/^ORD-[A-Z0-9]{8}$/.test(value)) {
    throw new Error("invalid_order_id");
  }
  return value as OrderId;
}

Brand از جعل عمدی با Assertion جلوگیری نمی‌کند؛ فقط خطای سهوی را سخت‌تر می‌کند. Factory یا Parser معتبر، بخش اصلی قرارداد است.

حالت‌ها را با Discriminated union مدل کنید

سه Boolean مانند isLoading، hasError و isPaid می‌توانند حالت‌های ناممکن بسازند. یک Union دارای Discriminant دقیق‌تر است:

type PaymentState =
  | { kind: "idle" }
  | { kind: "submitting"; requestId: string }
  | { kind: "requires_inquiry"; authority: string }
  | { kind: "succeeded"; receiptId: string; amountRial: bigint }
  | { kind: "failed"; reason: "rejected" | "timeout" | "invalid_response" };

function label(state: PaymentState): string {
  switch (state.kind) {
    case "idle": return "آماده";
    case "submitting": return "در حال ارسال";
    case "requires_inquiry": return "نیازمند استعلام";
    case "succeeded": return "پرداخت شد";
    case "failed": return "ناموفق";
    default: return assertNever(state);
  }
}

function assertNever(value: never): never {
  throw new Error(`unhandled state: ${JSON.stringify(value)}`);
}

مستند Narrowing و Discriminated union نشان می‌دهد چگونه TypeScript پس از بررسی فیلد مشترک، عضو Union را محدود و با never Exhaustiveness را بررسی می‌کند.

Optional، Null و «ناموجود» را یکی نکنید

این سه وضعیت می‌توانند معنی متفاوت داشته باشند:

  • Property وجود ندارد: مقدار هنوز ارسال نشده یا در این نسخه تعریف نشده است.
  • Property برابر undefined است: کلید وجود دارد اما مقدار تعریف نشده است.
  • Property برابر null است: فرستنده آگاهانه «بدون مقدار» را اعلام کرده است.

strictNullChecks خطای Null/Undefined را آشکار می‌کند. exactOptionalPropertyTypes نیز نبود Property را از prop: undefined دقیق‌تر جدا می‌کند. این تنظیم را با Contractهای واقعی و Migration سنجیده فعال کنید، نه برای افزایش امتیاز Config.

Generic باید رابطه را حفظ کند

Generic خوب رابطه ورودی و خروجی را بیان می‌کند؛ Generic بد فقط یک T تزئینی یا چندین Parameter غیرقابل‌فهم می‌سازد.

function first<T>(items: readonly T[]): T | undefined {
  return items[0];
}

// رابطه‌ای ندارد و unknown روشن‌تر است:
function logPayload(payload: unknown): void {
  console.info(payload);
}

Type و Interface؛ انتخاب کم‌اهمیت‌تر از قرارداد

هر دو برای بسیاری از Shapeهای Object مناسب‌اند. interface قابلیت Declaration merging دارد و برای برخی APIهای قابل گسترش مناسب است؛ type برای Union، Tuple و Composition بیان طبیعی‌تری دارد. تیم یک Convention کوتاه بنویسد، اما Review را صرف جنگ سلیقه‌ای نکند. سؤال مهم این است که آیا Type حالت واقعی و Boundary را درست نمایش می‌دهد.

مرزهای Runtime را با unknown آغاز کنید

هر داده‌ای که خارج از Process یا کنترل Type checker می‌آید، در نقطه ورود unknown است: JSON API، فرم، Header، Cookie، Queue، فایل، Environment variable، Local storage، Database قدیمی و پاسخ Vendor. Type declaration به داده واقعی فرمان نمی‌دهد.

Assertion اعتبارسنجی نیست

type GatewayResponse = {
  status: "ok";
  amountRial: number;
};

// خطرناک: فقط Compiler را ساکت می‌کند
const response = (await fetch(url).then(r => r.json())) as GatewayResponse;

در این نمونه هیچ کدی status یا amountRial را در Runtime بررسی نکرده است. نسخه امن، Parser یا Schema validator دارد و خطا را به Outcome شناخته‌شده تبدیل می‌کند:

type GatewayResponse = {
  status: "ok";
  amountRial: bigint;
};

function parseGatewayResponse(input: unknown): GatewayResponse {
  if (typeof input !== "object" || input === null) {
    throw new Error("gateway_invalid_object");
  }
  const value = input as Record<string, unknown>;
  if (value.status !== "ok" || typeof value.amountRial !== "string") {
    throw new Error("gateway_invalid_contract");
  }
  if (!/^\d+$/.test(value.amountRial)) {
    throw new Error("gateway_invalid_amount");
  }
  return { status: "ok", amountRial: BigInt(value.amountRial) };
}

در پروژه واقعی می‌توانید از کتابخانه Schema استفاده کنید، اما معیار انتخاب آن Bundle/Runtime cost، کیفیت Error، Type inference، قابلیت Versioning، Security، نگهداری و سازگاری محیط است. اسم Library جای Contract test را نمی‌گیرد.

چه زمانی any مجاز است؟

any کنترل Type را در مسیر انتشار می‌دهد و مانند یک سوراخ می‌تواند به مصرف‌کنندگان پخش شود. در Migration یا اتصال به Library بدون Type ممکن است موقتاً لازم باشد. آن را محدود کنید:

  • در یک Adapter باریک، نه Domain و API عمومی؛
  • با Issue، Owner، دلیل و تاریخ انقضا؛
  • با Test رفتاری برای مسیر واقعی؛
  • با Metric تعداد/روند، نه شعار «صفر any» فوری.

برای داده ناشناخته از unknown استفاده کنید تا مصرف‌کننده مجبور به Narrowing شود. @ts-ignore بدون Context خطا را پنهان می‌کند؛ @ts-expect-error دست‌کم اگر خطا ناپدید شود قابل تشخیص است، اما آن هم باید استثنای مالک‌دار باشد.

Type declaration طرف ثالث شاهد Runtime نیست

فایل .d.ts ممکن است قدیمی، ناقص یا ناسازگار با نسخه Runtime باشد. در Boundaryهای مهم، نسخه Package را Pin کنید، Contract test روی Artifact واقعی اجرا کنید و Upgrade را با Diff نوع و رفتار بسنجید. برای طراحی قرارداد HTTP، Versioning، Error، Idempotency و Webhook از راهنمای طراحی API وب استفاده کنید.

tsconfig یک قرارداد اجرایی است، نه فایل کپی‌شده

Config باید Runtime، Module system، Build tool، سطح Compatibility و سیاست خطای تیم را بازتاب دهد. یک Config مشهور را بدون فهم module، moduleResolution، target، lib و Emit کپی نکنید.

Baseline سخت‌گیرانه پیشنهادی

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true
  },
  "include": ["src", "test"]
}

این فقط نقطه شروع برای Applicationای است که ابزار دیگری Emit می‌کند. انتخاب Module/Resolution/Target باید جداگانه و بر اساس محیط باشد:

  • برای App مرورگری Bundleشده، ترکیب سازگار با Bundler همان پروژه را انتخاب کنید.
  • برای Node.js، nodenext و قواعد ESM/CommonJS واقعی Package را بسنجید.
  • برای Library، Declaration، Exports map، مصرف‌کننده‌ها و نسخه‌های هدف را Test کنید.
  • برای اجرای مستقیم Node با Type stripping، محدودیت Syntax و نادیده‌گرفتن tsconfig را بپذیرید.

مستند رسمی strict هشدار می‌دهد نسخه‌های آینده ممکن است Check سخت‌گیرانه تازه‌ای زیر این Flag اضافه کنند و Upgrade خطای جدید نشان دهد. این خطاها لزوماً Regression محصول نیستند، اما باید در Branch، با Owner و بودجه رفع بررسی شوند.

noUncheckedIndexedAccess چه مسئله‌ای را آشکار می‌کند؟

دسترسی items[0] یا map[key] همیشه مقدار تضمین‌شده ندارد. این Flag نتیجه را با undefined مدل می‌کند و تیم را به Check یا ساخت API دقیق‌تر وادار می‌سازد. هزینه آن می‌تواند Noise در Codebase قدیمی باشد؛ ابتدا در Module پایلوت فعال و خطاها را طبقه‌بندی کنید.

Emit و Type check را آگاهانه جدا کنید

بسیاری از Stackها با Bundler یا Transpiler JavaScript تولید می‌کنند و tsc --noEmit فقط Type check است. سرعت Build نباید با حذف Gate کیفیت به دست آید. سه مسیر را اندازه بگیرید:

  1. Feedback محلی و Editor؛
  2. Type check کامل در CI؛
  3. Emit/Bundle/Test مستقل.

Artifact را یک بار بسازید و همان Artifact آزموده‌شده را Deploy کنید. اصول Provenance، Gate، Cache و Rollback در راهنمای CI/CD امن آمده است.

Monorepo و Project references

Project references می‌تواند Codebase را به پروژه‌های کوچک‌تر تقسیم و Build order و مرز Declaration را آشکار کند. مستند رسمی Project references مزایا و Trade-offهایی مانند نیاز به composite، Declaration output و رفتار Editor را توضیح می‌دهد. آن را فقط وقتی اضافه کنید که Graph، Build time یا مرز Package مسئله واقعی است؛ صدها tsconfig به‌خودی‌خود معماری نمی‌سازد.

مهاجرت JavaScript به TypeScript بدون Big Bang

مهاجرت موفق یک پروژه مهندسی با Baseline، Scope و Exit gate است؛ نه مسابقه تغییر پسوند. راهنمای رسمی مهاجرت TypeScript استفاده از allowJs/checkJs و سخت‌گیری تدریجی را پشتیبانی می‌کند.

مرحله صفر: Baseline و Compatibility

  • نسخه Runtime، Framework، Bundler، Test runner، Linter و Pluginهای Editor را ثبت کنید.
  • زمان Type check/Build، تعداد Incidentهای Contract/Null/Shape و Lead time را اندازه بگیرید.
  • وابستگی‌های بدون Declaration یا دارای Type ناسازگار را Inventory کنید.
  • ماژول‌های پرریسک، پرتغییر و پرمصرف را نقشه کنید.
  • Rollback را با Lockfile و Branch/Canary مشخص کنید.

مرحله یک: JavaScript را داخل Project graph بیاورید

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": false,
    "noEmit": true
  },
  "include": ["src"]
}

سپس checkJs یا // @ts-check را روی فایل‌های منتخب فعال کنید. این مرحله Graph import، Typeهای Package و خطاهای واضح را نشان می‌دهد بدون آنکه همه فایل‌ها هم‌زمان Rename شوند.

مرحله دو: از Boundary یا Leaf شروع کنید؟

دو مسیر معتبر وجود دارد:

  • Boundary-first: اگر خطای API/Event/Config پرهزینه است، Parser و Contract آن را زود Type کنید.
  • Leaf-first: اگر Dependency graph پیچیده است، Utility یا Module کم‌وابستگی را برای یادگیری Toolchain انتخاب کنید.

برای پایلوت، یک Vertical slice با ورودی، منطق، خروجی و Test انتخاب کنید؛ یک Folder تزئینی که هیچ رفتار Production ندارد، ریسک واقعی را نشان نمی‌دهد.

مرحله سه: بودجه خطا و Suppression

هزار خطای اولیه را با any سراسری خاموش نکنید. خطاها را دسته‌بندی کنید:

دستهاقدامشاهد پایان
Bug واقعیاصلاح منطق + Regression testTest شکست‌خورده قبل و سبز بعد
Boundary ناشناختهParser/Schema + Error outcomeContract و Negative test
Declaration غلطUpgrade/Patch/Adapter باریکTest روی Runtime واقعی
مدل مبهمUnion/Domain type/Optional policyحذف حالت نامعتبر
Migration debtSuppression مالک‌دار و منقضیIssue، Deadline و روند نزولی

مرحله چهار: Strictness را بر اساس Risk بالا ببرید

برای Greenfield، strict را از ابتدا روشن کنید. برای Legacy، ابتدا Module پایلوت یا Config فرزند سخت‌گیر بسازید، سپس Scope را گسترش دهید. استثنا باید کوچک، مستند و موقت باشد. هدف «صفر Error در Configuration مصوب» است، نه صفر Type پیچیده یا صفر Assertion به هر قیمت.

مهاجرت به TypeScript ۷

اگر از نسخه‌های قدیمی می‌آیید، نخست روی نسخه انتقالی و Deprecationها کار کنید، سپس Compiler ۷ را کنار Pipeline فعلی آزمایش کنید. این Matrix را اجرا کنید:

  • CLI type check و Diagnostic diff؛
  • Editor navigation، Rename، Auto-import و Pluginها؛
  • Lint، Test، Coverage و Framework template checker؛
  • Declaration emit و مصرف Packageها؛
  • Cold/warm time، Memory و CI queue؛
  • Fallback به TypeScript ۶ برای Toolهای وابسته به API.

ادعای «۱۰ برابر سریع‌تر» را Forecast پروژه خود ندانید. Benchmark رسمی و تجربه شرکت‌ها جهت می‌دهند؛ عدد تصمیم باید از Repo و Runner واقعی شما بیاید.

Type safety بخشی از Quality system است

Compiler یک دسته خطا را پوشش می‌دهد. Coverage مطمئن، لایه‌ای است:

لایهپرسشنمونه
Type checkقراردادهای ایستا سازگارند؟ورودی تابع، Union exhaustiveness، Null
Unit/Propertyمنطق و Invariant درست‌اند؟محاسبه مبلغ و تبدیل ریال/تومان
Runtime schemaداده خارجی واقعاً Shape درست دارد؟پاسخ PSP، Env و Queue
Contract/Integrationدو سیستم روی Wire توافق دارند؟OpenAPI، Webhook، Database adapter
E2Eمسیر حیاتی در محیط واقعی کار می‌کند؟ثبت سفارش تا تأیید پرداخت
Observabilityشکست Production دیده و قابل تشخیص است؟invalid_contract rate و correlation ID

Generated type پایان Contract نیست

تولید Type از OpenAPI یا Schema، Drift دستی را کاهش می‌دهد؛ اما اگر Spec با Runtime همگام نباشد، Type تولیدی نیز غلط است. Producer contract test، Consumer test، Compatibility gate و Validation در مرز را نگه دارید. Type مشترک از یک Monorepo جای نسخه‌بندی Wire contract را نمی‌گیرد.

TypeScript و امنیت

Type می‌تواند API داخلی امن‌تر و State مجاز را روشن کند، اما ورودی مخرب را Sanitization یا Authorization نمی‌کند. رشته Typeشده همچنان می‌تواند Payload XSS یا SQL باشد؛ شناسه User Typeشده همچنان ممکن است متعلق به کاربر دیگری باشد. Encoding بر اساس Context، Query پارامتری، Authorization روی Resource، Secret management و Dependency controls مستقل‌اند. برای هزینه Runtime، Bundle، XSS و Supply chain به راهنمای عملکرد و امنیت اپ JavaScript رجوع کنید.

Performance را در جای درست اندازه بگیرید

TypeScript معمولاً Type annotation را از Runtime حذف می‌کند؛ پس مهاجرت خودبه‌خود App را سریع یا کند نمی‌کند. اما انتخاب Syntax نیازمند Transform، Polyfill، Target، Helper، Source map و Bundler می‌تواند Artifact را تغییر دهد. Bundle diff، Startup، Memory، P95/P99 و Error rate را روی خروجی نهایی بسنجید. برای عیب‌یابی از راهنمای پروفایلینگ وب‌اپلیکیشن استفاده کنید.

سنجه‌های برنامه TypeScript

  • زمان Feedback محلی، Type check کامل و CI queue؛
  • نرخ خطای Type/Contract/Null که پیش از Merge کشف می‌شود؛
  • تعداد و عمر any، Assertion و Suppression در Production code؛
  • درصد Boundaryهای خارجی دارای Runtime validation و Negative test؛
  • Change failure rate، Rollback و Incident مرتبط با Contract؛
  • زمان Onboarding و Review برای تغییرات چندماژولی؛
  • نرخ موفقیت Refactor و Files per change.

تعداد فایل .ts، تعداد Interface یا درصد پوشش Type به‌تنهایی Outcome نیست. اگر Build دو برابر شده و Incident تغییری نکرده، برنامه نیازمند بازطراحی است.

قواعد تیمی که Type debt را کنترل می‌کنند

  • Typeهای Public API و Boundary بازبین مشخص دارند.
  • هر Assertion پرریسک باید شاهد Runtime یا Invariant نزدیک داشته باشد.
  • any و Suppression در Code review دلیل، Scope و Expiry می‌خواهند.
  • Config مرکزی Versioned است، اما هر App تنظیم Runtime خودش را صریح دارد.
  • Compiler/Framework/Runtime upgrade در Branch با Matrix سازگاری انجام می‌شود.
  • Type پیچیده‌ای که Error آن برای تیم غیرقابل‌فهم است، ساده یا Encapsulate می‌شود.
  • Type-level test جای Test رفتاری و Runtime validation را نمی‌گیرد.

پنج Anti-pattern رایج

  1. Type assertion در ورودی API: داده را بدون Parse معتبر اعلام می‌کند.
  2. Boolean soup: چند پرچم، State ناممکن می‌سازند؛ Union مناسب‌تر است.
  3. Generic نمایشی: Complexity بدون رابطه مفید به API اضافه می‌کند.
  4. Shared types به‌عنوان Wire contract: Versioning و Runtime Drift را پنهان می‌کند.
  5. Big-bang strict: هزاران Error و Suppression ایجاد و اعتماد تیم را کم می‌کند.

مثال اجرایی: پرداخت در یک SaaS ایرانی

تیم یک سرویس Node.js، پنل React و دو PSP دارد. هر PSP نام فیلد، واحد مبلغ و Semantics خطای متفاوتی برمی‌گرداند. طراحی مناسب:

  1. پاسخ خام هر PSP در Adapter با unknown آغاز شود.
  2. Schema نسخه‌دار، مقدار و Format را در Runtime بررسی کند.
  3. Adapter واحد را به Type دامنه مانند amountRial: bigint Normalize کند.
  4. State پرداخت با Union و حالت requires_inquiry مدل شود؛ Timeout مساوی شکست قطعی نیست.
  5. Authorization، امضای Callback، Idempotency و Reconciliation مستقل از Type اجرا شوند.
  6. Contract test روی Sandbox و Fixtureهای ناشناخته/ناقص/تکراری اجرا شود.
  7. Metricهای invalid_contract، اختلاف مبلغ و سن Payment نامعلوم ثبت شوند.

TypeScript در این معماری از جابه‌جایی Type و فراموش‌شدن State جلوگیری می‌کند؛ Parser و Controlهای Runtime واقعیت بیرونی را مهار می‌کنند. انتخاب Node.js در برابر Go/Rust نیز تصمیم جداگانه‌ای است که در راهنمای مقایسه Backend بر اساس Workload و TCO بررسی شده است.

نقشه ۳۰/۶۰/۹۰روزه

روز ۱ تا ۳۰: تصمیم و پایلوت

  • Baseline خطا، Build، Review و Incident را ثبت کنید.
  • Compatibility matrix TypeScript ۶/۷ و Toolchain را بسازید.
  • یک Vertical slice واقعی و یک Boundary پرریسک انتخاب کنید.
  • Config پایه، Convention و سیاست any/Suppression را تصویب کنید.

روز ۳۱ تا ۶۰: Boundary و CI

  • ورودی‌های API/Env/Queue را با unknown و Runtime parser پوشش دهید.
  • Type check را به CI اضافه و زمان و Cache را اندازه بگیرید.
  • Unionهای State و Domain typeهای باارزش را جای Primitive obsession بنشانید.
  • Negative/Contract test و Telemetry خطای Validation بسازید.

روز ۶۱ تا ۹۰: گسترش یا توقف آگاهانه

  • Strictness را به Module بعدی با Risk و Capacity روشن گسترش دهید.
  • روند any/Assertion/Suppression و زمان CI را مرور کنید.
  • Change failure، Contract incident و Review time را با Baseline بسنجید.
  • تصمیم Scale/Hold/Repair/Rollback را ثبت کنید؛ درصد فایل مهاجرت‌کرده معیار اصلی نباشد.

چک‌لیست پذیرش و مهاجرت TypeScript

  • مسئله و Outcome مورد انتظار TypeScript تعریف شده است.
  • Runtime، Framework، Module و Build tool با Config هم‌راستا هستند.
  • strict برای Greenfield روشن و برای Legacy مسیر مرحله‌ای دارد.
  • تمام Boundaryهای خارجی از unknown و Validation عبور می‌کنند.
  • Assertion، any و Suppression محدود، مالک‌دار و قابل سنجش‌اند.
  • Type check مستقل از Emit و در CI الزام‌آور است.
  • Unit، Contract، Integration و E2E بر اساس ریسک حفظ شده‌اند.
  • نسخه ۷ در Toolchain واقعی و جریان‌های Embedded آزمایش شده است.
  • Build time، Incident و Developer experience قبل/بعد اندازه‌گیری می‌شوند.
  • Rollback و Owner ارتقای Compiler/Config روشن است.

پرسش‌های متداول

آیا TypeScript جای JavaScript را می‌گیرد؟

TypeScript بر Syntax و Runtime جاوااسکریپت بنا شده است و معمولاً Typeها را حذف و JavaScript تولید می‌کند. برای نوشتن TypeScript باید JavaScript را فهمید. برخی Runtimeها Syntax قابل‌حذف را مستقیم اجرا می‌کنند، اما این به معنی حذف نیاز به Type check یا فهم JavaScript نیست.

آیا TypeScript در Runtime برنامه را کند می‌کند؟

Type annotationها معمولاً از خروجی حذف می‌شوند و هزینه مستقیم Runtime ندارند. بااین‌حال Target، Transform، Polyfill، Helper، Runtime validator و Bundler می‌توانند Artifact را تغییر دهند. پاسخ قطعی از Measurement خروجی Production می‌آید، نه از نام زبان.

آیا TypeScript جای Runtime validation را می‌گیرد؟

خیر. Typeها پس از Compile پاک می‌شوند و داده API، فرم، Queue، Env و Database می‌تواند با Declaration ناسازگار باشد. ورودی خارجی را unknown بگیرید، Parse/Validate کنید و خطا را به Outcome قابل مشاهده تبدیل کنید.

برای پروژه JavaScript قدیمی از کجا شروع کنیم؟

Baseline و Compatibility را ثبت کنید، با allowJs/checkJs Graph را وارد TypeScript کنید و یک Vertical slice یا Boundary پرریسک را پایلوت بگیرید. خطاها را دسته‌بندی و Strictness را ماژول‌به‌ماژول گسترش دهید؛ Big bang معمولاً Suppression و بی‌اعتمادی می‌سازد.

آیا اکنون باید به TypeScript ۷ مهاجرت کنیم؟

فقط پس از آزمون Toolchain خود. نسخه ۷.۰ منتشر و برای Production ارائه شده، اما API برنامه‌نویسی پایدار ندارد و برخی Framework/Embedded-language workflowها هنوز به TypeScript ۶ وابسته‌اند. CLI، Editor، Lint، Template checker، Declaration emit، Build و Rollback را در Branch واقعی بسنجید.

جمع‌بندی

TypeScript یک Static type checker قدرتمند برای اکوسیستم JavaScript است؛ نه Validator Runtime، Framework معماری یا گواهی نبود Bug. ارزش آن از سه قرارداد می‌آید: Typeهایی که حالت واقعی را مدل می‌کنند، Boundaryهایی که داده ناشناخته را در Runtime می‌سنجند و Pipelineای که خطا و Drift را متوقف می‌کند.

کار را با شمارش فایل‌های .ts شروع نکنید. یک خطای پرهزینه و پرتکرار انتخاب کنید، مسیر ورود داده تا Outcome را Type و Validate کنید، سپس ببینید آیا Incident، Review و زمان تغییر واقعاً بهتر شده‌اند. اگر شاهد بهبود دارید، گسترش دهید؛ اگر نه، Config و Scope را اصلاح کنید.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *