نحو جدید const type parameter در TypeScript به یک تابع اجازه میدهد تا انواع لیترال (literal types) را بدون مجبور کردن فراخوانها به استفاده مکرر از as const در همه جا، دستنخورده نگه دارد و بدین ترتیب رایجترین منبع باگهای مربوط به گسترش نوع (type-widening) را کاهش دهد.
مشکل گسترش نوع (widening) که کدهای جنریک را آزار میدهد
وقتی یک تابع جنریک یک شیء لیترال (object literal) دریافت میکند، کامپایلر هر ویژگی لیترال را به نوع اولیه (primitive type) گستردهتر آن تبدیل میکند.
function call<T>(arg: T) {}
call({ method: "GET" }) // T is inferred as { method: string }
مقدار لیترال "GET" به string تبدیل میشود. کدهای پاییندستی که به مقدار دقیق وابسته هستند — مانند discriminated unions یا استخراج template-literal — از کار میافتند، زیرا نوع داده دیگر حامل آن لیترال دقیق نیست. توسعهدهندگان مدتهاست که این مشکل را با نوشتن { method: "GET" } as const در محل فراخوانی دور میزنند؛ این کار به کامپایلر میگوید که لیترال را حفظ کند، اما این راه حل در اختیار فراخوان (caller) قرار دارد، نه در تعریف تابع.
پارامترهای نوع const: راه حلی در سطح امضا (signature)
اصلاحکننده (modifier) جدید const روی یک پارامتر نوع، به کامپایلر میگوید که باریکترین نوع ممکن (narrowest possible type) را برای آن آرگومان جنریک استنتاج کند. تعریف یک تابع به صورت function foo<const T>(arg: T) باعث میشود T بهطور خودکار طوری رفتار کند که انگار فراخوان از as const استفاده کرده است.
- لیترالهای رشتهای، عددی و بولی مقادیر دقیق خود را حفظ میکنند (
"GET"به جایstring). - آرایهها به تاپلهای (tuples) خواندنی (readonly) تبدیل میشوند که هر عنصر آنها با نوع دقیق تعریف شده است.
- اشیاء به ساختارهای عمیقاً خواندنی تبدیل میشوند و انواع لیترال را در هر سطح از تو در تو بودن حفظ میکنند.
از آنجایی که این محدودیت در امضای تابع قرار دارد، هر فراخوانی بهطور خودکار از آن بهرهمند میشود؛ فراموش کردن یک cast دیگر منجر به ناپایداری (unsoundness) نمیشود.
چرا این روش بر ترفند کلاسیک as const برتری دارد
as const یک راه حل سمت فراخوان (caller-side) است. این روش مستلزم آن است که هر مصرفکننده یک تابع جنریک، اضافه کردن این assertion را به یاد داشته باشد. اگر حتی یک فراخوانی را فراموش کنید، امنیت نوع (type safety) از بین میرود. پارامتر نوع const، مسئولیت را به خودِ طراحی API منتقل میکند: تابع اعلام میکند که «من به باریکترین شکلِ هر آنچه شما پاس میدهید نیاز دارم» و کامپایلر آن را اعمال میکند.
این تغییر بیش از همه برای کتابخانهها و ابزارهایی اهمیت دارد که builderهای جنریک، کارخانههای پیکربندی (configuration factories) یا هر API ای را ارائه میدهند که در آن مقدار لیترال یک فیلد، منطق نوع را هدایت میکند. نویسنده کتابخانه میتواند بدون نظارت بر کدهای پاییندستی، استنتاج صحیح را تضمین کند.
سناریوهای دنیای واقعی که از این ویژگی بهره میبرند
- سازندگان پیکربندی (Configuration builders) – نام محیطها (
"dev" | "prod") لیترال باقی میمانند و بررسیهای discriminated-union را بدون نیاز به castهای اضافی امکانپذیر میکنند. - تعاریف مسیرهای API – رشتههای مسیر دقیق باقی میمانند و به انواع template-literal اجازه میدهند پارامترها را استخراج کنند (
"/users/:id"→\/users/${string}``). - کمککنندههای ماشین حالت (State-machine helpers) – شناسههای حالت از طریق زنجیرهسازی متدها (method chaining) به صورت لیترالهای ثابت باقی میمانند و از عدم تطابق تصادفی حالتها جلوگیری میکنند.
در هر مورد، پارامتر const کدهای تکراری (boilerplate) مربوط به as const را حذف کرده و احتمال نفوذ باگهای ظریف را کاهش میدهد.
ترکیب با عملگر satisfies
عملگر satisfies تأیید میکند که یک مقدار با یک نوع ساختاری مطابقت دارد و در عین حال اطلاعات لیترال اصلی آن را حفظ میکند. استفاده همزمان از هر دو، بهترین ویژگیهای هر دو را در اختیار شما قرار میدهد: پارامترهای const استنتاج باریک را فراهم میکنند و satisfies تضمین میکند که مقدار با شکل مورد نظر مطابقت داشته باشد.
function makeConfig<const C>(cfg: C) {
// cfg is inferred with exact literals
}
const cfg = {
env: "staging",
ports: [8080, 8443],
} satisfies { env: string; ports: number[] };
makeConfig(cfg); // works, literals stay intact
چه زمانی از as const استفاده کنیم
پارامتر const زمانی میدرخشد که شما بر امضای تابع کنترل دارید. اگر با توابع شخص ثالث (third-party) سروکار دارید که فاقد این اصلاحکننده هستند، یا نیاز به حفظ لیترال برای یک متغیر محلی دارید، as const همچنان ابزار مناسبی است. این روش همچنان به عنوان راه اصلی برای منجمد کردن (freeze) یک مقدار بدون تغییر در API فراخوانی شده، عمل میکند.
آنچه باید در آینده دنبال کنید
این ویژگی هنوز جدید است، بنابراین ابزارها و الگوهای جامعه کاربری در حال تکامل هستند. منتظر بهروزرسانیهای پشتیبانی IDE باشید که نحو جدید را در تکمیل خودکار (autocomplete) و پیشنهادهای اصلاح سریع (quick-fix) نمایش میدهند. نگاهی به نگهدارندگان کتابخانهها داشته باشید: بسیاری از آنها شروع به مهاجرت جنریکهای عمومی به پارامترهای const خواهند کرد، که ممکن است باعث ایجاد تغییرات شکستبرانگیز (breaking changes) برای کدهایی شود که قبلاً به castهای صریح as const متکی بودند.
نکته کلیدی: با گنجاندن حفظ لیترال مستقیماً در پارامترهای نوع یک تابع، پارامترهای نوع const در TypeScript یک منبع رایج از خطاهای گسترش نوع را حذف کرده و مسئولیت امنیت را از فراخوان به طراح API بازمیگردانند. از آنها برای هر نقطه ورود جنریک که تحت کنترل خود دارید استفاده کنید؛ as const را برای مقادیر محلی یا APIهای خارجی رزرو کنید.
