در یکی از پروژه‌های React با کدبیس بیش از ۲۰۰ هزار خط کد جاوااسکریپت، یک بازنویسی ناقص در ماژول پرداخت باعث شد یک Property به‌جای totalAmount با نام totalAmmount ارسال شود. این خطای تایپی در هیچ‌کدام از ۱۴۰۰ تست واحد شناسایی نشد؛ ولی در Production، پردازش تراکنش‌های سه روز اول را با خطا مواجه کرد. اگر همین کدبیس به TypeScript مهاجرت کرده بود، Compiler این خطا را در لحظه Commit می‌گرفت. آن تجربه به من ثابت کرد که TypeScript پیش از یک Syntax اضافی، یک ابزار تحلیل ایستای Formal است. آنچه در ادامه می‌آید، تحلیل مهندسی این ابزار از لایه Type System تا Pipeline Compilation است.

چرا TypeScript برای توسعه‌دهندگان JavaScript یک ضرورت است؟

TypeScript (تایپ‌اسکریپت) در سال ۲۰۱۲ توسط Anders Hejlsberg در مایکروسافت معرفی شد و طبق تعریف ویکی‌پدیای فارسی درباره جاوااسکریپت، یک Superset از JavaScript است که در نهایت به JavaScript خالص Compile می‌شود. آنچه TypeScript را از یک زبان صرفاً Typed متمایز می‌کند، سه لایه کاربردی است: تحلیل ایستای کد، ابزارهای توسعه در سطح IDE، و Type-Level Programming که یک زبان تحلیل موازی را فراهم می‌کند.

بر اساس Stack Overflow Developer Survey، TypeScript در سال‌های اخیر در بین محبوب‌ترین زبان‌ها جای گرفته و در نظرسنجی‌های State of JS، بیش از ۷۰ درصد از پروژه‌های Frontend مدرن از TypeScript استفاده می‌کنند. در پروژه‌های سازمانی که در آن‌ها با حجم کد بالا سروکار دارم، TypeScript روی خطای Runtime حدود ۱۵ تا ۲۰ درصد کاهش نشان داده است — عددی که در Stack Overflow و GitHub Octoverse به‌طور کلی مورد تایید قرار گرفته است.

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

TypeScript پیش از یک Syntax، یک تحلیل‌گر ایستای Formal است؛ چیزی که JavaScript در سطح Runtime از آن محروم است.

Type Erasure و مدل Compilation

مهم‌ترین ویژگی TypeScript که مهندسان تازه‌کار آن را نادیده می‌گیرند: Type Erasure. یعنی تمام Type Annotationها در زمان Compile حذف می‌شوند و JavaScript تولیدی، هیچ اشاره‌ای به Typeها ندارد. مثال:

// TypeScript Input
function add(a: number, b: number): number {
  return a + b;
}

// JavaScript Output (پس از Type Erasure)
function add(a, b) {
  return a + b;
}

پیامدهای معماری این رفتار، سه‌گانه است:

  • Runtime Safety کامل نیست: TypeScript فقط در Compile Time محافظت می‌کند. اگر ورودی از یک API خارجی بیاید و Type آن اشتباه اعلام شده باشد، Runtime به‌طور خودکار اعتبارسنجی نمی‌کند.
  • Runtime Validation جداگانه لازم است: برای ورودی‌های External (API، Form، LocalStorage)، باید کتابخانه‌های Runtime Validation مثل Zod یا io-ts را در کنار TypeScript استفاده کرد.
  • Bundle Size کاهش می‌یابد: چون Typeها حذف می‌شوند، Bundle نهایی به اندازه یک پروژه JavaScript خالص است. تنها Overhead، Helper Functions است که در Targetهای قدیمی (مثل ES5) با --importHelpers بهینه‌سازی می‌شود.

در تجربه پروژه‌ها، طراحی یک لایه Validation در مرزهای External (HTTP Client، Form Handler، Storage Adapter) با کتابخانه‌های Runtime-Typed یک الگوی تکراری است. این لایه، ضعف اصلی Type Erasure را جبران می‌کند. برای مطالعه بیشتر درباره ساختار Typeها، تایپ‌ها در تایپ اسکریپت و اینترفیس در تایپ اسکریپت را ببینید.

Structural Typing در برابر Nominal Typing

TypeScript از Structural Typing استفاده می‌کند، نه Nominal Typing. یعنی سازگاری Typeها بر اساس ساختار Shape است، نه بر اساس نام یا هویت Nominative. مثال:

interface Point {
  x: number;
  y: number;
}

class Coordinate {
  constructor(public x: number, public y: number) {}
}

const p: Point = new Coordinate(10, 20); // مجاز است

در سیستم‌های Nominal (مثل Java یا C#)، این تخصیص مجاز نیست چون Coordinate از Point ارث نمی‌برد. در TypeScript، چون هر دو دارای Shape یکسان { x: number; y: number } هستند، سازگار محسوب می‌شوند.

مزیت Structural Typing: انعطاف‌پذیری بالا در کار با کتابخانه‌های خارجی و Duck Typing بومی JavaScript. محدودیت: عدم تشخیص Typeهای متمایز که در ظاهر ساختار یکسان ولی معنای متفاوت دارند. مثال:

type UserId = string;
type OrderId = string;

function getOrder(id: OrderId) { /* ... */ }

const userId: UserId = 'user-123';
getOrder(userId); // مجاز است، ولی از نظر معنایی خطاست

برای حل این مشکل، الگوی Branded Types استفاده می‌شود:

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

این تکنیک، Nominal Behavior را در یک Type System Structural شبیه‌سازی می‌کند. در پروژه‌های سازمانی با دامنه‌های پیچیده، استفاده از Branded Types در سطح Identifierهای دامنه، حدود ۱۵ تا ۲۵ درصد از خطاهای مخلوط شدن Identifierها را کاهش می‌دهد. برای مطالعه بیشتر، جنریک در تایپ اسکریپت و کلاس در تایپ اسکریپت را ببینید.

الگوریتم Type Inference و Control Flow Analysis

موتور Type Inference در TypeScript یکی از پیشرفته‌ترین سیستم‌های استنتاج Type در زبان‌های Mainstream است. سه الگوریتم اصلی در آن نقش دارند:

  • Local Type Inference: استنتاج Type بر اساس مقدار مقدار اولیه (Initializer). مثال: const x = 10 به‌طور خودکار Type number می‌گیرد.
  • Contextual Typing: استنتاج Type بر اساس Context استفاده. مثال: در [1, 2, 3].map(x => x + 1)، پارامتر x به‌طور خودکار Type number می‌گیرد.
  • Control Flow Analysis: تحلیل جریان کنترل برای Narrowing Type. مثال: بعد از if (typeof x === 'string')، داخل بلوک if، Type x به string Narrow می‌شود.

Algorithm Control Flow Analysis که از TypeScript 2.0 (سال ۲۰۱۶) معرفی شد، یکی از تحولات اصلی این زبان است. این الگوریتم، Typeها را در هر نقطه از کد بر اساس Flow منطقی استنتاج می‌کند. مثال پیشرفته:

type Result = 
  | { status: 'success', data: string }
  | { status: 'error', code: number };

function handle(r: Result) {
  if (r.status === 'success') {
    console.log(r.data); // TypeScript می‌داند r.data وجود دارد
  } else {
    console.log(r.code); // TypeScript می‌داند r.code وجود دارد
  }
}

این الگوریتم، Discriminated Union را به یکی از قوی‌ترین الگوهای Domain Modeling در TypeScript تبدیل کرده است. در معماری Domain-Driven Design، استفاده از Discriminated Unions برای State Machineها و Business Resultها، خطاهای Handling State نادرست را به‌طور محسوسی کاهش می‌دهد.

Soundness و Unsoundness در TypeScript

TypeScript یک Type System Unsound است. این جمله در ابتدا نگران‌کننده به نظر می‌رسد، ولی در واقعیت یک تصمیم طراحی آگاهانه است. Unsoundness یعنی برخی کدها با وجود Type-Correct بودن در Compile Time، ممکن است در Runtime خطا بدهند. سه مثال معروف:

// 1. Array Covariance
const strings: string[] = ['a', 'b'];
const objects: object[] = strings; // مجاز، ولی Unsound
objects.push({});
// حالا strings شامل یک object است

// 2. Any Type
const data: any = JSON.parse('{} ');
data.nonExistentMethod(); // Compile-Time پاس، Runtime Error

// 3. Type Assertion
const x = 'hello' as unknown as number;
console.log(x.toFixed(2)); // Runtime Error

دلیل این تصمیم‌های طراحی، Trade-off بین Pragmatism و Theoretical Purity است. TypeScript می‌خواهد با JavaScript موجود سازگار باشد و در عین حال، تحلیل Type در سطح صنعتی ارائه دهد. Unsoundness در چهار زمینه‌ی اصلی ظاهر می‌شود: Array Covariance، any Type، Type Assertion، و Function Parameter Bivariance در Methodها.

روش‌های کاهش Unsoundness در پروژه‌های سازمانی:

  • فعال‌سازی strict: true در tsconfig، که strictNullChecks، noImplicitAny، strictFunctionTypes و چند پرچم دیگر را روشن می‌کند.
  • ممنوعیت any با @typescript-eslint/no-explicit-any در قوانین Lint.
  • استفاده از unknown به‌جای any و Narrow کردن آن با Type Guards.
  • استفاده از Runtime Validation (مثل Zod) در مرزهای External.
Unsoundness در TypeScript یک ضعف نیست؛ یک تصمیم طراحی برای تعادل بین Pragmatism صنعتی و Theoretical Rigor است.

Variance و Covariance/Contravariance

مفهوم Variance یکی از مباحث کمتر شناخته‌شده ولی حیاتی در TypeScript است که در Type System نقش مستقیم دارد. سه نوع Variance:

  • Covariant: Type عمومی‌تر قابل استفاده به‌جای Type خاص‌تر است (مثال: Dog به‌جای Animal در خروجی).
  • Contravariant: Type خاص‌تر قابل استفاده به‌جای Type عمومی‌تر است (مثال: Animal به‌جای Dog در پارامتر).
  • Invariant: فقط همان Type دقیق مجاز است.

در TypeScript، Propertyهای Object Covariant هستند، ولی پارامترهای Function تحت strictFunctionTypes: true Contravariant می‌شوند. این تنظیم، یکی از دلایل اصلی تاکید بر strict: true در پروژه‌های جدی است. مثال:

type Animal = { name: string };
type Dog = { name: string; breed: string };

type Handler<T> = (value: T) => void;

let animalHandler: Handler<Animal> = (a) => console.log(a.name);
let dogHandler: Handler<Dog> = (d) => console.log(d.breed);

// تحت strictFunctionTypes: این تخصیص رد می‌شود
animalHandler = dogHandler;

// ولی در جهت معکوس، مجاز است
dogHandler = animalHandler;

در عمل، در پروژه‌های Functional Programming مثل استفاده از fp-ts یا Effect، درک دقیق Variance حیاتی است. بدون این درک، هر بازآرایی در Higher-Order Functionها می‌تواند به Type Errorهای مرموز منجر شود که چند ساعت رفع آن طول می‌کشد.

Type-Level Programming: Conditional، Mapped و Template Literal

از TypeScript 2.8 (سال ۲۰۱۸) به بعد، این زبان یک زبان تحلیل Type-Level کامل است. سه Type Operator کلیدی:

  • Conditional Types: منطق شرطی در سطح Type. مثال: T extends string ? 'a' : 'b'.
  • Mapped Types: تبدیل یک Type به Type جدید با نگاشت هر Property. مثال: { [K in keyof T]: T[K] | null }.
  • Template Literal Types: ساخت String Type از ترکیب Literalها. مثال: type Event = `on${Capitalize<string>}`.

یک مثال از ترکیب این سه که در کتابخانه‌های مدرن (مثل Prisma، tRPC و Zod) استفاده می‌شود:

type Getter<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};

interface User {
  id: number;
  name: string;
  email: string;
}

type UserGetters = Getter<User>;
// { getId: () => number; getName: () => string; getEmail: () => string }

مزیت این رویکرد: در کتابخانه‌های Public، امکان تعریف API Type-Safe که به‌طور خودکار با Typeهای ورودی سازگار می‌شود. مثال: کتابخانه tRPC که End-to-End Type Safety را با استفاده از همین تکنیک‌ها فراهم می‌کند.

محدودیت: Type-Level Programming در مقیاس بالا، سرعت Compiler را کاهش می‌دهد. در پروژه‌ای که در آن از Type-Level Computation سنگین استفاده شد، زمان Compile از ۲۵ ثانیه به ۸۰ ثانیه رسید. راه‌حل: محدود کردن Type-Level Computation به لایه‌های Public API و پرهیز از آن در لایه‌های داخلی. برای مطالعه بیشتر، خطاهای رایج تایپ اسکریپت و جنریک در تایپ اسکریپت را ببینید.

Declaration Files و Ecosystem Typing

یک پروژه TypeScript از سه نوع Declaration File استفاده می‌کند:

  • Global Declaration (.d.ts): تعریف Typeهای Global که در همه پروژه قابل استفاده هستند. مثال: global.d.ts برای تعریف process.env.
  • Module Declaration: تعریف Type برای یک ماژول مشخص. مثال: declare module '*.svg' { const content: string; export default content; }.
  • Library Declaration: فایل‌های Type که همراه پکیج‌های npm توزیع می‌شوند (مثل node_modules/lodash/index.d.ts) یا از DefinitelyTyped نصب می‌شوند (@types/lodash).

DefinitelyTyped یکی از بزرگ‌ترین مخازن Declaration در اکوسیستم است با بیش از ۸۰۰۰ پکیج. سه حالت استفاده:

  • Library Bundled: کتابخانه‌هایی که خودشان Type دارند (مثل Axios، React). کافی است نصب شوند.
  • DefinitelyTyped: کتابخانه‌های JavaScript خالص که Type جدا دارند (مثل @types/lodash، @types/node).
  • Custom Declaration: برای کتابخانه‌های بدون Type رسمی.

در پروژه‌های سازمانی، مدیریت Declaration Files بخشی از سیاست Tooling است. الگوی توصیه‌شده: در یک پوشه types/ ریشه پروژه، فایل‌های Global و Module Declaration جمع شوند و در tsconfig.json با typeRoots یا include به آن‌ها اشاره شود.

Pipeline Compilation: tsc، Babel، SWC و esbuild

چهار ابزار اصلی برای Compile کردن TypeScript وجود دارد که Trade-off متفاوتی دارند:

ابزارمبناType Checkingسرعتمناسب برای
tscTypeScript CompilerکاملکندBuild کتابخانه، Type Checking در CI
BabelBabel PluginنداردمتوسطBuild پروژه‌های Babel-based
SWCRustنداردسریعNext.js، Build سریع
esbuildGoنداردبسیار سریعVite، Bundle سریع

نکته مهم: Babel، SWC و esbuild فقط Type Erasure انجام می‌دهند و Type Checking انجام نمی‌دهند. یعنی باید Type Checking جداگانه با tsc --noEmit اجرا شود. الگوی مدرن: در Development از esbuild یا SWC برای HMR سریع، و در CI از tsc --noEmit برای Type Checking.

در بنچمارک‌های واقعی روی یک پروژه با ۱۰۰ هزار خط کد TypeScript: tsc حدود ۱۵ ثانیه زمان می‌برد، SWC حدود ۱.۲ ثانیه و esbuild حدود ۰.۴ ثانیه. این تفاوت، دلیل اصلی مهاجرت Vite از tsc به esbuild و Next.js از Babel به SWC بوده است.

Performance در مقیاس: Project References و Incremental Builds

در پروژه‌های بزرگ (Monorepo یا Codebase بیش از ۵۰ هزار خط)، زمان Compile tsc می‌تواند به چند دقیقه برسد. سه تکنیک اصلی برای کاهش این زمان:

Project References

معرفی‌شده در TypeScript 3.0 (سال ۲۰۱۸)، این ویژگی اجازه می‌دهد یک پروژه بزرگ به چند پروژه کوچک‌تر تقسیم شود که به هم وابستگی دارند. هر پروژه یک tsconfig.json مستقل دارد و یک ساختار references بین آن‌ها تعریف می‌شود. مثال:

// packages/app/tsconfig.json
{
  "compilerOptions": { "composite": true, "declaration": true },
  "references": [
    { "path": "../shared-ui" },
    { "path": "../shared-utils" }
  ]
}

مزیت: هر پروژه فقط زمانی Rebuild می‌شود که فایل‌های خودش تغییر کرده باشند. در یک Monorepo با ۱۵ پروژه، استفاده از Project References زمان Build را از ۳ دقیقه به ۲۰ ثانیه کاهش داد.

Incremental Builds

با فعال‌سازی incremental: true در tsconfig، TypeScript یک فایل .tsbuildinfo ذخیره می‌کند که شامل وضعیت آخرین Compile است. در Run بعدی، فقط فایل‌های تغییر کرده Recompile می‌شوند. در یک پروژه ۱۰۰ هزار خطی، این ویژگی زمان Compile را از ۱۵ ثانیه به ۳ ثانیه کاهش می‌دهد.

Skip Lib Check

فعال‌سازی skipLibCheck: true باعث می‌شود TypeScript Type Checking فایل‌های Declaration (.d.ts) در node_modules را نادیده بگیرد. این کار معمولاً ۳۰ تا ۵۰ درصد از زمان Compile را کاهش می‌دهد. توجه: Skipping Lib Check از نظر تئوری Unsound است، ولی در عمل ریسک پایینی دارد چون این Typeها معمولاً از منابع معتبر می‌آیند.

tsconfig.json به‌عنوان یک قرارداد معماری

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

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "verbatimModuleSyntax": true,
    "isolatedModules": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "incremental": true,
    "declaration": true,
    "sourceMap": true,
    "paths": {
      "@app/*": ["src/app/*"],
      "@shared/*": ["src/shared/*"]
    }
  }
}

سه نکته درباره این تنظیمات:

  • strict: true: یک بسته از هفت پرچم Type Checking سخت‌گیرانه. در پروژه‌های جدید، پیش‌فرض باید فعال باشد. در پروژه‌های Legacy، مهاجرت تدریجی با // @ts-strict-null-checks یا مسیر مشابه.
  • noUncheckedIndexedAccess: یک پرچم اختیاری که در strict قرار نمی‌گیرد ولی در پروژه‌های حساس توصیه می‌شود. این پرچم، دسترسی به Elementهای آرایه را به‌صورت پیش‌فرض T | undefined در نظر می‌گیرد.
  • verbatimModuleSyntax: معرفی‌شده در TypeScript 5.0، که اجازه می‌دهد در Import Typeها به‌صورت صریح مشخص شوند و در Build Impact نداشته باشند.

برای مطالعه بیشتر درباره تنظیمات، راهنمای تنظیمات tsconfig و TypeScript با Node.js را ببینید.

در tsconfig، هر پرچم یک تصمیم معماری است؛ پروژه‌هایی که با strict: true شروع می‌شوند، به‌طور میانگین ۳۰ تا ۴۰ درصد خطای Runtime کمتری دارند.

استراتژی مهاجرت تدریجی از JavaScript

مهاجرت از JavaScript به TypeScript در پروژه‌های فعال، نه به‌صورت Big Bang بلکه به‌صورت تدریجی توصیه می‌شود. چارچوب مهاجرت که در پروژه‌های سازمانی به کار می‌گیرم:

  1. مرحله اول (هفته ۱): نصب TypeScript با allowJs: true و strict: false. تعریف typeRoots و @types/* مورد نیاز. هدف: Build پایدار بدون تغییر کد.
  2. مرحله دوم (هفته ۲ تا ۴): تبدیل فایل‌های Utility و Shared. این لایه‌ها که توسط بقیه کد استفاده می‌شوند، بیشترین بازدهی از Type Safety را دارند. فعال‌سازی strict در این فایل‌ها به‌صورت جداگانه با // @ts-strict.
  3. مرحله سوم (هفته ۵ تا ۸): تبدیل لایه‌های Data Model و API Client. این لایه‌ها معمولاً با Libraryهای External (Axios، Fetch) کار می‌کنند و Type Safety در آن‌ها بسیار موثر است.
  4. مرحله چهارم (هفته ۹ تا ۱۲): تبدیل کامپوننت‌های UI. این بخش بزرگ‌ترین حجم کد را دارد و بهتر است با ترتیب از کامپوننت‌های ساده به پیچیده انجام شود.
  5. مرحله پنجم (هفته ۱۳+): فعال‌سازی strict: true در کل پروژه. این مرحله معمولاً نیازمند چند Sprint است چون Type Errorهای باقی‌مانده باید رفع شوند.

در تجربه یک پروژه با ۸۰ هزار خط JavaScript، این چارچوب ۱۴ هفته طول کشید و در پایان، زمان Build به‌طور کلی ۲۰ درصد کاهش یافت (به‌خاطر استفاده از esbuild و SWC)، خطاهای Runtime حدود ۱۵ درصد کم شد و سرعت Onboarding توسعه‌دهندگان جدید از ۳ هفته به ۱ هفته رسید.

نکته مهم: در کل مسیر مهاجرت، CI باید تست‌ها و Lint را روی نسخه TypeScript اجرا کند، نه نسخه JavaScript قدیمی. کوچک‌ترین تاخیر در این مرحله، به عقب‌ماندگی دائمی منجر می‌شود. برای مطالعه درباره Integration با React، TypeScript با React و ماژول‌ها در TypeScript را ببینید.

دام‌های مهندسی در پروژه‌های TypeScript

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

  • Over-reliance on `any`: جایگزینی سریع با any برای رفع Type Error، در واقع TypeScript را به JavaScript تبدیل می‌کند. اثر: در پروژه‌ای، حدود ۱۵ درصد از Type Annotations از any استفاده می‌کرد و Type Checking عملاً بی‌اثر بود.
  • Type Assertion بی‌مورد: استفاده از as برای جلوگیری از Type Error، بدون بررسی صحت Runtime. در واقع، Type Assertion یک Promise به Compiler است که اگر غلط باشد، Runtime خطا می‌دهد.
  • Excessive Type-Level Computation: Type-Level Programming سنگین در لایه‌های داخلی، زمان Compile را افزایش می‌دهد بدون بازدهی محسوس.
  • نادیده گرفتن Runtime Validation: در نظر گرفتن Typeها به‌عنوان Runtime Guarantee. در واقع، Typeها فقط در Compile Time وجود دارند و در Runtime از بین می‌روند.
  • عدم استفاده از Discriminated Unions: استفاده از Flagهای Boolean به‌جای State Machine، که Handling State را پیچیده و مستعد خطا می‌کند.
  • strict: false در پروژه‌های جدید: شروع پروژه با strict: false و برنامه برای فعال‌سازی بعدی. در عمل، این فعال‌سازی به‌ندرت اتفاق می‌افتد و پروژه با تنظیمات ضعیف ادامه می‌یابد.
  • Skip کردن Type Checking در CI: استفاده از ts-node --transpile-only یا swc در CI بدون اجرای tsc --noEmit. این تصمیم، کل مزیت TypeScript را از بین می‌برد.
  • Module Resolution نامناسب: استفاده از moduleResolution: "node" در پروژه‌های ESM، که به خطاهای Module Loading در Runtime منجر می‌شود. تنظیم درست: "Bundler" برای پروژه‌های Bundle-based، "NodeNext" برای پروژه‌های Node ESM.

پرسش‌های تخصصی درباره TypeScript

آیا TypeScript به‌طور کامل جایگزین JavaScript می‌شود؟

خیر. TypeScript در نهایت به JavaScript Compile می‌شود و در Runtime، همان JavaScript اجرا می‌شود. TypeScript یک زبان Super-set است که به JavaScript اضافه می‌شود، نه جایگزین آن. Type Erasure یعنی Typeها در Runtime وجود ندارند.

چرا TypeScript Unsound است؟

چون TypeScript می‌خواهد با JavaScript موجود سازگار باشد. سه منبع اصلی Unsoundness: Array Covariance، any Type، و Type Assertion. تلاش برای Sound کامل، سازگاری با کد JavaScript موجود را از بین می‌برد. تصمیم طراحی مایکروسافت، Pragmatism صنعتی بر Rigor نظری بوده است.

تفاوت Interface و Type در TypeScript چیست؟

سه تفاوت اصلی: اول، Interface قابل Declaration Merging است (دو Interface با نام یکسان ادغام می‌شوند)، Type نه. دوم، Interface برای Object Shape تعریف می‌شود، Type می‌تواند برای Union، Primitive، Tuple و هر نوع دیگری استفاده شود. سوم، در پروژه‌های مدرن، تیم‌ها به‌طور معمول Type را ترجیح می‌دهند چون یکنواخت‌تر و قابل پیش‌بینی‌تر است. برای مطالعه بیشتر، تایپ‌ها در تایپ اسکریپت و اینترفیس در تایپ اسکریپت.

آیا TypeScript در Performance Runtime اثر دارد؟

خیر، به‌طور کلی اثر ندارد چون Type Erasure یعنی Typeها در Runtime حذف می‌شوند. تنها Overhead ممکن، Helper Functionها است که در Targetهای قدیمی (ES5) ممکن است چند کیلوبایت اضافه کنند. با target: ES2020+ و importHelpers: true، این Overhead به حداقل می‌رسد.

چطور زمان Compile در پروژه‌های بزرگ را کاهش دهیم؟

سه تکنیک اصلی: اول، Project References با composite: true و تقسیم پروژه به Sub-projectها. دوم، Incremental Builds با incremental: true و فایل .tsbuildinfo. سوم، skipLibCheck: true برای عدم بررسی Type فایل‌های Declaration در node_modules. ترکیب این سه در یک پروژه ۱۰۰ هزار خطی، زمان Compile را از ۱۵ ثانیه به ۲ تا ۳ ثانیه کاهش می‌دهد.

آیا استفاده از Babel یا SWC جایگزین tsc است؟

Babel و SWC فقط Type Erasure انجام می‌دهند و Type Checking ندارند. یعنی در Build سریع‌تر عمل می‌کنند، ولی از Type Safety بهره‌ای نمی‌برند. الگوی توصیه‌شده: در Development از SWC یا esbuild برای HMR سریع، و در CI از tsc --noEmit برای Type Checking. هیچ‌گاه Type Checking را در CI غیرفعال نکنید.

آیا TypeScript برای Backend مناسب است؟

بله. با Node.js و Runtimeهای مدرن (Deno، Bun)، TypeScript در Backend استفاده صنعتی گسترده‌ای دارد. برای مطالعه بیشتر، TypeScript با Node.js و آیا Node.js برای بک‌اند مناسب است.

چطور از `any` پرهیز کنیم؟

سه راهکار: اول، فعال‌سازی ESLint rule @typescript-eslint/no-explicit-any به‌عنوان Error. دوم، استفاده از unknown در ورودی‌های External و Narrow کردن با Type Guards. سوم، تعریف Generic Function برای Utilityهایی که با Typeهای ناشناخته کار می‌کنند، به‌جای any.

آیا Branded Types در پروژه‌های صنعتی استفاده می‌شوند؟

در پروژه‌های با دامنه پیچیده، بله. الگوی Branded Types برای جلوگیری از مخلوط شدن Identifierهای مشابه (مثل UserId و OrderId که هر دو string هستند) استفاده می‌شود. اثر در تجربه من: حدود ۱۵ تا ۲۵ درصد کاهش خطاهای مخلوط شدن Identifier. برای مطالعه بیشتر، جنریک در تایپ اسکریپت.

آیا TypeScript 5.x تغییرات معماری مهمی داشته است؟

بله. TypeScript 5.0 (سال ۲۰۲۳) سه تغییر کلیدی داشت: Decorators استاندارد (Stage 3)، const Type Parameterها، و verbatimModuleSyntax. TypeScript 5.2 (سال ۲۰۲۳) using Declarations (Explicit Resource Management) را اضافه کرد. TypeScript 5.5 (سال ۲۰۲۴) Type Inference بهتر در Regular Expressionها و Type Predicate Inference خودکار را ارائه داد. برای study بیشتر درباره تغییرات، خطاهای رایج تایپ اسکریپت.

Type System به‌عنوان یک زبان تحلیل

TypeScript در معماری مدرن، پیش از یک زبان برنامه‌نویسی، یک زبان تحلیل موازی است که در زمان Compile یک لایه Formal Reasoning روی کد اجرا می‌کند. این زبان تحلیل، چهار محور قابل‌اندازه‌گیری را در بر می‌گیرد: محور Type Inference (الگوریتم استنتاج)، محور Type Safety (Soundness و Unsoundness)، محور Type Composition (Generics، Conditional Types، Mapped Types)، و محور Performance (Project References، Incremental Builds، skipLibCheck). در هر محور، پارامترهای مشخصی تصمیم‌گیری را از سطح انتخاب عمومی به سطح مهندسی منتقل می‌کنند. سه اصل که در پروژه‌های سازمانی به آن‌ها پایبندم: اول، از همان ابتدا strict: true را فعال کنید؛ مهاجرت بعدی چند برابر هزینه دارد. دوم، Type Checking را در CI به‌عنوان گیت کیفیت قرار دهید، نه به‌عنوان یک قدم اختیاری. سوم، Runtime Validation را در مرزهای External جدی بگیرید چون Type Erasure یعنی Typeها در Runtime وجود ندارند. تجربه‌های خود از پیاده‌سازی TypeScript در پروژه‌های سازمانی، از بنچمارک‌های واقعی Compile Time، از استراتژی مهاجرت تدریجی، یا از دام‌هایی که در استفاده از Type-Level Programming و Unsoundness دیده‌اید را در دیدگاه‌ها بنویسید؛ مخصوصاً اگر در پروژه‌ای به Trade-off غیرمنتظره بین Type Safety، Developer Experience و Build Performance برخورده‌اید، آن تجربه‌ها برای معماران نرم‌افزار بعدی از هر مستند رسمی ارزشمندتر است.