اخبار وب

TypeScript 6 و Type-level Programming؛ تایپ امن و مسیر ارتقا

جایگاه TypeScript 6 در مسیر نسخه 7، تایپ‌های پیشرفته، اعتبارسنجی runtime و مثال کامل Union برای ساخت مدل امن از داده JSON را بشناسید.

1404/11/17 2 دقیقه مطالعه 2,517 بازدید
TypeScript 6 و Type-level Programming؛ تایپ امن و مسیر ارتقا

یک پاسخ API می‌رسد که ظاهراً شکل درستی دارد، اما مقدار status آن با چیزی که برنامه انتظار دارد فرق می‌کند. اگر فقط با عبارت as آن را به تایپ دلخواه تبدیل کرده باشیم، TypeScript اعتراضی نمی‌کند و خطا چند مرحله بعد، هنگام نمایش رابط کاربری ظاهر می‌شود. همین مثال کوچک نشان می‌دهد ارزش سیستم تایپ به تعداد تایپ‌های پیچیده نیست؛ به این است که مرز اعتماد را درست تعریف کنیم و اشتباه را جایی ببینیم که هنوز اصلاحش کم‌هزینه است.

TypeScript 6.0 در ۲۳ مارس ۲۰۲۶ منتشر شد و نقش مهمی در گذار ابزارهای این زبان دارد. با این حال، Type-level Programming از این نسخه شروع نشده و تایپ‌های بازگشتی هم بی‌محدودیت نشده‌اند. امروز که TypeScript 7 نیز منتشر شده، بهتر است نسخه ششم را با جایگاه واقعی‌اش بشناسیم: فرصتی برای اصلاح تنظیمات و وابستگی‌ها، همراه با استفاده دقیق‌تر از قابلیت‌هایی که پیش‌تر در زبان وجود داشتند.

نسخه ششم چه جایگاهی دارد؟

در اعلام انتشار رسمی TypeScript 6.0، این نسخه پلی میان کامپایلر مبتنی بر JavaScript و نسل native معرفی شده است. تغییرات آن فقط اضافه شدن چند تعریف تایپ نیست؛ بعضی پیش‌فرض‌ها و تنظیمات قدیمی تغییر کرده‌اند و پروژه‌ای که سال‌ها با پیکربندی قبلی کار کرده، ممکن است هنگام ارتقا خطاهای تازه ببیند. چنین خطایی همیشه نشانه خراب شدن کد برنامه نیست؛ گاهی پیکربندی قبلی چیزی را به‌صورت ضمنی فرض می‌کرده است.

TypeScript 7.0 در ۸ ژوئیه ۲۰۲۶ منتشر شد و بازنویسی native مبتنی بر Go را به نسخه پایدار رساند. بنابراین نسبت دادن این بازنویسی به نسخه ششم دقیق نیست. گزارش سرعت در مطلب رسمی نسخه هفتم درباره ساخت و بررسی تایپ پروژه‌های مشخص است؛ به معنی سریع‌تر شدن JavaScript خروجی یا پاسخ API شما نیست. زمان اجرای محصول همچنان به الگوریتم، شبکه، پایگاه داده و معماری آن بستگی دارد.

Type-level Programming در عمل یعنی چه؟

وقتی از یک تایپ موجود، تایپ دیگری می‌سازیم، در سطح تایپ رابطه تعریف کرده‌ایم. مثلاً مدل ذخیره‌شده مقاله شناسه، عنوان و وضعیت دارد، اما فرم ایجاد مقاله نباید شناسه تعیین کند. می‌توان تایپ ورودی فرم را از مدل اصلی استخراج کرد تا تغییر نام یک فیلد در چند جای مستقل فراموش نشود. این رابطه هنگام بررسی کد معنا دارد و قرار نیست در زمان اجرای برنامه، داده‌ای را پاک یا تغییر دهد.

Generics کمک می‌کنند رابطه میان ورودی و خروجی یک تابع حفظ شود. keyof نام کلیدهای یک تایپ را به دست می‌دهد و indexed access نوع مقدار یک کلید را استخراج می‌کند. Mapped Types روی کلیدها حرکت می‌کنند و Conditional Types براساس رابطه تایپ‌ها نتیجه می‌سازند. این ابزارها در راهنمای رسمی ساخت تایپ از تایپ توضیح داده شده‌اند. استفاده مفیدشان معمولاً کوچک و نزدیک به نیاز محصول است، نه یک معمای طولانی که فقط نویسنده‌اش بتواند بخواند.

برای نمونه، تیمی که فرم ویرایش پروفایل می‌سازد می‌تواند مجموعه مشخصی از فیلدهای قابل ویرایش را انتخاب کند. اما استفاده مستقیم از Partial برای هر عملیات کافی نیست: ممکن است همه فیلدها اختیاری شوند، در حالی که محصول حداقل یک تغییر واقعی می‌خواهد. این شرط باید در منطق برنامه بررسی شود. تایپ خوب بخشی از قرارداد را روشن می‌کند؛ جای قواعد کسب‌وکار و اعتبارسنجی ورودی را نمی‌گیرد.

مرز میان تایپ و اعتبارسنجی داده

داده شبکه، ورودی فرم، localStorage و فایل واردشده از بیرون، صرفاً به دلیل وجود یک interface معتبر نمی‌شوند. تایپ‌های TypeScript پس از تبدیل به JavaScript عمدتاً حذف می‌شوند. عبارت as یک ادعا برای کامپایلر است و بررسی عملی تولید نمی‌کند. اگر پاسخ سرور را بدون بررسی به Article تبدیل کنیم، مسئولیت درست بودن آن را خودمان پذیرفته‌ایم؛ این ادعا تضمین نمی‌کند title واقعاً رشته باشد.

برای ورودی نامطمئن، unknown شروع بهتری از any است. با unknown پیش از دسترسی به داده باید نوع آن را مشخص کنیم. می‌توان این کار را با بررسی‌های مستقیم یا یک کتابخانه schema انجام داد. در راهنمای اعتبارسنجی با Zod و TypeScript، این مرز با جزئیات بیشتری مطرح شده است. انتخاب روش به تعداد مدل‌ها و نیاز به پیام خطا بستگی دارد؛ یک پروژه کوچک الزاماً به وابستگی تازه احتیاج ندارد.

یک مثال کامل: دریافت و بررسی وضعیت یک کار

مثال زیر سه وضعیت برای کار تعریف می‌کند. کار باز عنوان دارد، کار انجام‌شده علاوه بر عنوان زمان تکمیل دارد و داده نامعتبر با پیام مشخص رد می‌شود. هدف، ساخت یک Task از JSON است؛ نه اعتماد کردن به JSON با یک تبدیل ظاهری تایپ. تابع parseTask ابتدا شکل ورودی را بررسی می‌کند و سپس مقدار خروجی را به‌صورت مشخص می‌سازد تا فیلدهای اضافی ورودی ناخواسته وارد مدل داخلی نشوند.

برای اجرای نمونه به Node.js و npm نیاز دارید. پوشه تازه‌ای بسازید، فایل زیر را با نام task.ts در آن ذخیره کنید و دستورات بعد از کد را اجرا کنید. این نمونه به framework، HTML یا فایل پنهان دیگری وابسته نیست و خروجی را در ترمینال نمایش می‌دهد. نصب typescript@6 نسخه اصلی مورد بحث را نگه می‌دارد؛ استفاده از نسخه بدون شماره ممکن است نسل دیگری از کامپایلر را نصب کند.

type Task =
  | { status: "open"; title: string }
  | { status: "done"; title: string; completedAt: string };

type ParseResult<T> =
  | { ok: true; value: T }
  | { ok: false; error: string };

function parseTask(input: unknown): ParseResult<Task> {
  if (typeof input !== "object" || input === null || Array.isArray(input)) {
    return { ok: false, error: "ورودی باید یک شیء باشد." };
  }
  if (!("title" in input) || typeof input.title !== "string" || !input.title.trim()) {
    return { ok: false, error: "عنوان کار معتبر نیست." };
  }
  const title = input.title.trim();
  if ("status" in input && input.status === "open") {
    return { ok: true, value: { status: "open", title } };
  }
  if ("status" in input && input.status === "done") {
    if (!("completedAt" in input) || typeof input.completedAt !== "string") {
      return { ok: false, error: "زمان تکمیل لازم است." };
    }
    const time = new Date(input.completedAt);
    if (!Number.isFinite(time.getTime()) || time.toISOString() !== input.completedAt) {
      return { ok: false, error: "زمان تکمیل باید ISO معتبر با UTC باشد." };
    }
    return { ok: true, value: { status: "done", title, completedAt: input.completedAt } };
  }
  return { ok: false, error: "وضعیت کار پشتیبانی نمی‌شود." };
}

function describeTask(task: Task): string {
  switch (task.status) {
    case "open": return `کار باز: ${task.title}`;
    case "done": return `انجام شد: ${task.title} — ${task.completedAt}`;
    default: {
      const unreachable: never = task;
      throw new Error(`Unexpected task: ${String(unreachable)}`);
    }
  }
}

const samples: unknown[] = [
  JSON.parse('{"status":"open","title":"بازبینی مقاله"}'),
  JSON.parse('{"status":"done","title":"انتشار راهنما","completedAt":"2026-10-10T09:00:00.000Z"}'),
  JSON.parse('{"status":"open","title":42}'),
];

for (const sample of samples) {
  const result = parseTask(sample);
  console.log(result.ok ? describeTask(result.value) : result.error);
}
npm init -y
npm install --save-dev typescript@6
npx tsc task.ts --strict --target ES2022 --module commonjs --outDir dist --noEmitOnError
node dist/task.js

خروجی نمونه اول متن کار باز، نمونه دوم متن کار انجام‌شده و نمونه سوم پیام نامعتبر بودن عنوان است. مقادیر متن و زمان در خود فایل قرار دارند و هیچ درخواست اینترنتی ارسال نمی‌شود. اگر تاریخ تکمیل خالی باشد یا با فرمت ISO مورد انتظار سازگار نباشد، داده پذیرفته نمی‌شود. این قاعده را برای نمایش مفهوم تعریف کرده‌ایم؛ محصول واقعی باید درباره timezone و معنای زمان تکمیل نیز قرارداد روشنی داشته باشد.

چرا این Union از چند فیلد اختیاری بهتر است؟

اگر completedAt را برای تمام کارها اختیاری کنیم، حالتی مثل status برابر done بدون زمان تکمیل در مدل مجاز می‌شود. Discriminated Union به‌جای آن، هر وضعیت را با داده لازم خودش تعریف می‌کند. با بررسی status، کامپایلر می‌فهمد کدام شاخه در دسترس است. این روش برای نتیجه پرداخت، وضعیت بارگذاری، درخواست تأیید و جریان انتشار مقاله نیز کاربرد دارد؛ هر جا چند حالت داریم که اطلاعاتشان یکسان نیست.

در تابع describeTask، بررسی never کمک می‌کند اضافه شدن وضعیت جدید از چشم منطق نمایش پنهان نماند. اگر بعداً وضعیت cancelled به Task اضافه شود ولی نمایش آن را ننویسیم، بررسی تایپ خطا می‌دهد. این رفتار به قرارداد تایپ وابسته است، نه بررسی خودکار تمام حالت‌های ممکن محصول. ورودی همچنان باید از parseTask عبور کند. توضیح Narrowing و تشخیص شاخه‌ها را می‌توانید در مستندات رسمی Narrowing بخوانید.

هنگام ارتقا، تنظیمات را صریح کنید

در نسخه ششم، strict به‌طور پیش‌فرض فعال است، rootDir پیش‌فرض به مسیر فایل پیکربندی وابسته شده و types پیش‌فرض آرایه خالی است. اگر پروژه به تایپ‌های سراسری Node یا ابزار دیگری تکیه می‌کند، ممکن است لازم باشد آن‌ها را صریح معرفی کنید. برای مثال، استفاده از process در پروژه Node با نمونه ساده‌ای که فقط console دارد یکسان نیست؛ وجود بسته تایپ و تنظیمات درست باید با محیط واقعی هماهنگ باشد.

پیکربندی را براساس runtime و ابزار ساخت بنویسید. target مشخص می‌کند چه سطحی از JavaScript خروجی مناسب است، ولی API گمشده مرورگر را به برنامه اضافه نمی‌کند. lib هم تعریف تایپ را فراهم می‌کند، نه polyfill. اگر کامپایلر متدی را می‌شناسد، هنوز باید بدانید مرورگر یا نسخه Node مورد استفاده مشتری آن را اجرا می‌کند یا نه. این تفاوت جلوی بسیاری از خطاهایی را می‌گیرد که در محیط توسعه دیده نمی‌شوند.

تایپ‌های پیچیده چه هزینه‌ای دارند؟

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

پیچیدگی را نزدیک به جایی نگه دارید که ارزش می‌سازد. کتابخانه عمومی ممکن است برای حفظ inference به تایپ‌های پیشرفته نیاز داشته باشد، اما در یک فرم داخلی، دو مدل صریح خواناترند. استفاده از any برای خاموش کردن خطا یا افزودن assertion پشت assertion معمولاً نشانه اختلاف میان مدل و داده است. پیش از پیچیده‌تر کردن تایپ، نمونه واقعی ورودی و خروجی را روی کاغذ مشخص کنید و ببینید قرارداد کجا شکسته است.

یک مسیر عملی برای تیم توسعه

ابتدا نسخه کامپایلر، تنظیمات و وابستگی‌هایی را که از API آن استفاده می‌کنند ثبت کنید. ارتقای کامپایلر را با تغییر framework و بازنویسی معماری در یک مرحله ادغام نکنید؛ در غیر این صورت پیدا کردن منشأ خطا دشوار می‌شود. سپس مرزهای داده نامطمئن را مشخص کنید: پاسخ API، داده ذخیره‌شده و ورودی کاربر. اصلاح همین مرزها معمولاً بیشتر از اضافه کردن تایپ‌های تزئینی به کدهای داخلی به کیفیت محصول کمک می‌کند.

در مسیر نسخه هفتم، سازگاری ابزارهایی که برنامه‌وار از کامپایلر استفاده می‌کنند نیز مهم است؛ صرف نصب کامپایلر تازه کافی نیست. مستندات همان ابزار و پیکربندی editor را بررسی کنید و درباره زمان بررسی تایپ، پیام خطا و حافظه با داده پروژه خود تصمیم بگیرید. مقاله Design Tokens و همکاری طراحی و توسعه نمونه دیگری از ارزش قراردادهای مشترک است: نام‌گذاری و قرارداد روشن، قبل از پیچیدگی ابزار، به هماهنگی تیم کمک می‌کنند.

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

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!