راهنمای جامع توابع assert در TypeScript. یاد بگیرید چگونه بین زمان کامپایل و اجرا پل بزنید، دادهها را اعتبارسنجی کنید و با مثالهای عملی، کدی امنتر و قویتر بنویسید.
توابع Assert در TypeScript: راهنمای جامع برای ایمنی نوع در زمان اجرا
در دنیای توسعه وب، قرارداد بین انتظارات کد شما و واقعیت دادههایی که دریافت میکند، اغلب شکننده است. TypeScript با ارائه یک سیستم نوع استاتیک قدرتمند، انقلابی در نحوه نوشتن جاوااسکریپت ایجاد کرده و بیشمار باگ را قبل از رسیدن به محیط پروداکشن شناسایی میکند. با این حال، این شبکه ایمنی عمدتاً در زمان کامپایل وجود دارد. چه اتفاقی میافتد وقتی برنامه زیبای تایپشده شما، دادههای نامرتب و غیرقابل پیشبینی را از دنیای خارج در زمان اجرا دریافت میکند؟ اینجاست که توابع assertion تایپاسکریپت به ابزاری ضروری برای ساخت برنامههای واقعاً قوی تبدیل میشوند.
این راهنمای جامع شما را به یک بررسی عمیق از توابع assertion میبرد. ما بررسی خواهیم کرد که چرا آنها ضروری هستند، چگونه آنها را از ابتدا بسازیم، و چگونه آنها را در سناریوهای رایج دنیای واقعی به کار ببریم. در پایان، شما برای نوشتن کدی مجهز خواهید شد که نه تنها در زمان کامپایل از نظر نوع ایمن است، بلکه در زمان اجرا نیز مقاوم و قابل پیشبینی است.
شکاف بزرگ: زمان کامپایل در مقابل زمان اجرا
برای درک واقعی توابع assertion، ابتدا باید چالش اساسی که آنها حل میکنند را بفهمیم: شکاف بین دنیای زمان کامپایل TypeScript و دنیای زمان اجرای جاوااسکریپت.
بهشت زمان کامپایل TypeScript
وقتی کد TypeScript مینویسید، در بهشت یک توسعهدهنده کار میکنید. کامپایلر TypeScript (tsc
) به عنوان یک دستیار هوشیار عمل میکند و کد شما را بر اساس انواع تعریفشدهتان تجزیه و تحلیل میکند. این موارد را بررسی میکند:
- ارسال انواع نادرست به توابع.
- دسترسی به ویژگیهایی که روی یک شیء وجود ندارند.
- فراخوانی متغیری که ممکن است
null
یاundefined
باشد.
این فرآیند قبل از اجرای کد شما اتفاق میافتد. خروجی نهایی، جاوااسکریپت خالص است که تمام حاشیهنویسیهای نوع از آن حذف شدهاند. TypeScript را به عنوان یک نقشه معماری دقیق برای یک ساختمان در نظر بگیرید. این نقشه تضمین میکند که تمام طرحها صحیح هستند، اندازهگیریها دقیق هستند، و یکپارچگی ساختاری روی کاغذ تضمین شده است.
واقعیت زمان اجرای جاوااسکریپت
هنگامی که TypeScript شما به جاوااسکریپت کامپایل شده و در مرورگر یا محیط Node.js اجرا میشود، انواع استاتیک از بین رفتهاند. کد شما اکنون در دنیای پویا و غیرقابل پیشبینی زمان اجرا عمل میکند. باید با دادههایی از منابعی که کنترلی بر آنها ندارد، سر و کار داشته باشد، مانند:
- پاسخهای API: یک سرویس بکاند ممکن است ساختار داده خود را به طور غیرمنتظره تغییر دهد.
- ورودی کاربر: دادههای فرمهای HTML همیشه به عنوان رشته در نظر گرفته میشوند، صرف نظر از نوع ورودی.
- Local Storage: دادههای بازیابی شده از
localStorage
همیشه یک رشته هستند و نیاز به تجزیه (parse) دارند. - متغیرهای محیطی: اینها اغلب رشته هستند و ممکن است اصلاً وجود نداشته باشند.
برای استفاده از تشبیه ما، زمان اجرا محل ساخت و ساز است. نقشه بینقص بود، اما مصالح تحویل داده شده (دادهها) ممکن است اندازه اشتباه، نوع اشتباه یا به سادگی غایب باشند. اگر سعی کنید با این مصالح معیوب بسازید، سازه شما فرو میریزد. اینجاست که خطاهای زمان اجرا رخ میدهند که اغلب منجر به کرشها و باگهایی مانند "Cannot read properties of undefined" میشود.
ورود توابع Assertion: پل زدن بر شکاف
بنابراین، چگونه نقشه TypeScript خود را بر روی مصالح غیرقابل پیشبینی زمان اجرا اعمال کنیم؟ ما به مکانیزمی نیاز داریم که بتواند دادهها را همانطور که میرسند بررسی کرده و تأیید کند که با انتظارات ما مطابقت دارند. این دقیقاً کاری است که توابع assertion انجام میدهند.
تابع Assertion چیست؟
یک تابع assertion نوع خاصی از تابع در TypeScript است که دو هدف حیاتی را دنبال میکند:
- بررسی در زمان اجرا: این تابع یک اعتبارسنجی بر روی یک مقدار یا شرط انجام میدهد. اگر اعتبارسنجی شکست بخورد، یک خطا پرتاب میکند و فوراً اجرای آن مسیر کد را متوقف میکند. این کار از انتشار دادههای نامعتبر در برنامه شما جلوگیری میکند.
- محدود کردن نوع در زمان کامپایل: اگر اعتبارسنجی موفقیتآمیز باشد (یعنی خطایی پرتاب نشود)، به کامپایلر TypeScript سیگنال میدهد که نوع مقدار اکنون خاصتر شده است. کامپایلر به این assertion اعتماد میکند و به شما اجازه میدهد از آن مقدار به عنوان نوع assert شده برای بقیه دامنه آن استفاده کنید.
جادو در امضای تابع است که از کلمه کلیدی asserts
استفاده میکند. دو شکل اصلی وجود دارد:
asserts condition [is type]
: این شکل assert میکند که یکcondition
خاص truthy است. شما میتوانید به صورت اختیاریis type
(یک گزاره نوع) را نیز اضافه کنید تا نوع یک متغیر را نیز محدود کنید.asserts this is type
: این در متدهای کلاس برای assert کردن نوع کانتکستthis
استفاده میشود.
نکته کلیدی، رفتار «پرتاب خطا در صورت شکست» است. برخلاف یک بررسی ساده if
، یک assertion اعلام میکند: «این شرط باید برای ادامه برنامه درست باشد. اگر اینطور نیست، این یک وضعیت استثنایی است و باید فوراً متوقف شویم.»
ساخت اولین تابع Assertion: یک مثال عملی
بیایید با یکی از رایجترین مشکلات در جاوااسکریپت و TypeScript شروع کنیم: سر و کار داشتن با مقادیر بالقوه null
یا undefined
.
مشکل: Null های ناخواسته
تصور کنید تابعی یک شیء کاربر اختیاری را میگیرد و میخواهد نام کاربر را لاگ کند. بررسیهای دقیق null در TypeScript به درستی در مورد یک خطای احتمالی به ما هشدار میدهد.
interface User {
name: string;
email: string;
}
function logUserName(user: User | undefined) {
// 🚨 خطای TypeScript: 'user' احتمالاً 'undefined' است.
console.log(user.name.toUpperCase());
}
راه استاندارد برای رفع این مشکل، استفاده از یک بررسی if
است:
function logUserName(user: User | undefined) {
if (user) {
// داخل این بلوک، TypeScript میداند که 'user' از نوع 'User' است.
console.log(user.name.toUpperCase());
} else {
console.error('User is not provided.');
}
}
این کار میکند، اما اگر undefined
بودن `user` در این زمینه یک خطای غیرقابل جبران باشد چه؟ ما نمیخواهیم تابع بیصدا ادامه دهد. ما میخواهیم با صدای بلند شکست بخورد. این منجر به گارد کلازهای (guard clauses) تکراری میشود.
راه حل: یک تابع Assertion به نام `assertIsDefined`
بیایید یک تابع assertion قابل استفاده مجدد برای مدیریت زیبا این الگو ایجاد کنیم.
// تابع assertion قابل استفاده مجدد ما
function assertIsDefined<T>(value: T, message: string = "Value is not defined"): asserts value is NonNullable<T> {
if (value === undefined || value === null) {
throw new Error(message);
}
}
// بیایید از آن استفاده کنیم!
interface User {
name: string;
email: string;
}
function logUserName(user: User | undefined) {
assertIsDefined(user, "User object must be provided to log name.");
// بدون خطا! TypeScript اکنون میداند که 'user' از نوع 'User' است.
// نوع از 'User | undefined' به 'User' محدود شده است.
console.log(user.name.toUpperCase());
}
// مثال استفاده:
const validUser = { name: 'Alice', email: 'alice@example.com' };
logUserName(validUser); // "ALICE" را لاگ میکند
const invalidUser = undefined;
try {
logUserName(invalidUser); // یک خطا پرتاب میکند: "User object must be provided to log name."
} catch (error) {
console.error(error.message);
}
تجزیه امضای Assertion
بیایید امضا را تجزیه کنیم: asserts value is NonNullable<T>
asserts
: این کلمه کلیدی ویژه TypeScript است که این تابع را به یک تابع assertion تبدیل میکند.value
: این به اولین پارامتر تابع اشاره دارد (در مورد ما، متغیری به نام `value`). این به TypeScript میگوید که نوع کدام متغیر باید محدود شود.is NonNullable<T>
: این یک گزاره نوع است. این به کامپایلر میگوید که اگر تابع خطا پرتاب نکند، نوع `value` اکنونNonNullable<T>
است. نوع ابزارNonNullable
در TypeScript،null
وundefined
را از یک نوع حذف میکند.
موارد استفاده عملی برای توابع Assertion
اکنون که اصول اولیه را درک کردیم، بیایید بررسی کنیم که چگونه توابع assertion را برای حل مشکلات رایج و واقعی به کار ببریم. آنها در مرزهای برنامه شما، جایی که دادههای خارجی و بدون نوع وارد سیستم شما میشوند، قدرتمندترین هستند.
مورد استفاده ۱: اعتبارسنجی پاسخهای API
این مسلماً مهمترین مورد استفاده است. دادههای حاصل از یک درخواست fetch
ذاتاً غیرقابل اعتماد هستند. TypeScript به درستی نتیجه `response.json()` را به عنوان `Promise
سناریو
ما در حال واکشی دادههای کاربر از یک API هستیم. انتظار داریم که با رابط `User` ما مطابقت داشته باشد، اما نمیتوانیم مطمئن باشیم.
interface User {
id: number;
name: string;
email: string;
}
// یک تایپ گارد معمولی (یک بولین برمیگرداند)
function isUser(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
'id' in data && typeof (data as any).id === 'number' &&
'name' in data && typeof (data as any).name === 'string' &&
'email' in data && typeof (data as any).email === 'string'
);
}
// تابع assertion جدید ما
function assertIsUser(data: unknown): asserts data is User {
if (!isUser(data)) {
throw new TypeError('Invalid User data received from API.');
}
}
async function fetchAndProcessUser(userId: number) {
const response = await fetch(`https://api.example.com/users/${userId}`);
const data: unknown = await response.json();
// شکل داده را در مرز برنامه assert کنید
assertIsUser(data);
// از این نقطه به بعد، 'data' به طور ایمن به عنوان 'User' تایپ شده است.
// دیگر نیازی به بررسیهای 'if' یا تبدیل نوع نیست!
console.log(`Processing user: ${data.name.toUpperCase()} (${data.email})`);
}
fetchAndProcessUser(1);
چرا این قدرتمند است: با فراخوانی `assertIsUser(data)` درست پس از دریافت پاسخ، ما یک «دروازه ایمنی» ایجاد میکنیم. هر کدی که در ادامه میآید میتواند با اطمینان با `data` به عنوان یک `User` رفتار کند. این کار منطق اعتبارسنجی را از منطق تجاری جدا میکند و منجر به کدی بسیار تمیزتر و خواناتر میشود.
مورد استفاده ۲: اطمینان از وجود متغیرهای محیطی
برنامههای سمت سرور (مانند Node.js) به شدت به متغیرهای محیطی برای پیکربندی وابسته هستند. دسترسی به `process.env.MY_VAR` نوع `string | undefined` را نتیجه میدهد. این شما را مجبور میکند که وجود آن را در هر جایی که از آن استفاده میکنید بررسی کنید، که خستهکننده و مستعد خطا است.
سناریو
برنامه ما برای شروع به یک کلید API و یک URL پایگاه داده از متغیرهای محیطی نیاز دارد. اگر آنها وجود نداشته باشند، برنامه نمیتواند اجرا شود و باید فوراً با یک پیام خطای واضح کرش کند.
// در یک فایل ابزار، مثلاً 'config.ts'
export function getEnvVar(key: string): string {
const value = process.env[key];
if (value === undefined) {
throw new Error(`FATAL: Environment variable ${key} is not set.`);
}
return value;
}
// یک نسخه قدرتمندتر با استفاده از assertions
function assertEnvVar(key: string): asserts key is keyof NodeJS.ProcessEnv {
if (process.env[key] === undefined) {
throw new Error(`FATAL: Environment variable ${key} is not set.`);
}
}
// در نقطه ورود برنامه شما، مثلاً 'index.ts'
function startServer() {
// تمام بررسیها را در هنگام راهاندازی انجام دهید
assertEnvVar('API_KEY');
assertEnvVar('DATABASE_URL');
const apiKey = process.env.API_KEY;
const dbUrl = process.env.DATABASE_URL;
// TypeScript اکنون میداند که apiKey و dbUrl رشته هستند، نه 'string | undefined'.
// برنامه شما تضمین میکند که پیکربندی مورد نیاز را دارد.
console.log('API Key length:', apiKey.length);
console.log('Connecting to DB:', dbUrl.toLowerCase());
// ... بقیه منطق راهاندازی سرور
}
startServer();
چرا این قدرتمند است: این الگو «شکست سریع» (fail-fast) نامیده میشود. شما تمام پیکربندیهای حیاتی را یک بار در همان ابتدای چرخه حیات برنامه خود اعتبارسنجی میکنید. اگر مشکلی وجود داشته باشد، فوراً با یک خطای توصیفی شکست میخورد، که اشکالزدایی آن بسیار آسانتر از یک کرش مرموز است که بعداً هنگام استفاده نهایی از متغیر گمشده رخ میدهد.
مورد استفاده ۳: کار با DOM
وقتی از DOM کوئری میگیرید، برای مثال با `document.querySelector`، نتیجه `Element | null` است. اگر مطمئن هستید که یک عنصر وجود دارد (مثلاً `div` ریشه اصلی برنامه)، بررسی مداوم برای `null` میتواند دست و پا گیر باشد.
سناریو
ما یک فایل HTML با `
` داریم و اسکریپت ما باید محتوا را به آن متصل کند. ما میدانیم که وجود دارد.
// استفاده مجدد از assertion عمومی قبلی ما
function assertIsDefined<T>(value: T, message: string = "Value is not defined"): asserts value is NonNullable<T> {
if (value === undefined || value === null) {
throw new Error(message);
}
}
// یک assertion خاصتر برای عناصر DOM
function assertQuerySelector<T extends Element>(selector: string, constructor?: new () => T): T {
const element = document.querySelector(selector);
assertIsDefined(element, `FATAL: Element with selector '${selector}' not found in the DOM.`);
// اختیاری: بررسی کنید که آیا نوع عنصر درست است یا خیر
if (constructor && !(element instanceof constructor)) {
throw new TypeError(`Element '${selector}' is not an instance of ${constructor.name}`);
}
return element as T;
}
// استفاده
const appRoot = document.querySelector('#app-root');
assertIsDefined(appRoot, 'Could not find the main application root element.');
// پس از assertion، appRoot از نوع 'Element' است، نه 'Element | null'.
appRoot.innerHTML = 'Hello, World!
';
// استفاده از helper خاصتر
const submitButton = assertQuerySelector<HTMLButtonElement>('#submit-btn', HTMLButtonElement);
// 'submitButton' اکنون به درستی به عنوان HTMLButtonElement تایپ شده است
submitButton.disabled = true;
چرا این قدرتمند است: این به شما امکان میدهد یک ثبات (invariant) - شرطی که میدانید درست است - را در مورد محیط خود بیان کنید. این کار کدهای پر سر و صدای بررسی null را حذف میکند و به وضوح وابستگی اسکریپت به یک ساختار DOM خاص را مستند میکند. اگر ساختار تغییر کند، شما یک خطای فوری و واضح دریافت میکنید.
توابع Assertion در مقابل جایگزینها
دانستن اینکه چه زمانی از یک تابع assertion در مقابل سایر تکنیکهای محدود کردن نوع مانند تایپ گاردها یا تبدیل نوع استفاده کنید، بسیار مهم است.
تکنیک | سینتکس | رفتار در صورت شکست | بهترین کاربرد |
---|---|---|---|
تایپ گاردها (Type Guards) | value is Type |
false برمیگرداند |
کنترل جریان (if/else ). زمانی که یک مسیر کد جایگزین و معتبر برای حالت «نامطلوب» وجود دارد. مثلاً، «اگر رشته است، آن را پردازش کن؛ در غیر این صورت، از یک مقدار پیشفرض استفاده کن.» |
توابع Assertion | asserts value is Type |
یک Error پرتاب میکند |
اعمال ثباتها (invariants). زمانی که یک شرط باید برای ادامه صحیح برنامه درست باشد. مسیر «نامطلوب» یک خطای غیرقابل جبران است. مثلاً، «پاسخ API باید یک شیء User باشد.» |
تبدیل نوع (Type Casting) | value as Type |
بدون تأثیر در زمان اجرا | موارد نادری که شما، به عنوان توسعهدهنده، بیشتر از کامپایلر میدانید و قبلاً بررسیهای لازم را انجام دادهاید. این روش ایمنی زمان اجرا را صفر میکند و باید با احتیاط استفاده شود. استفاده بیش از حد از آن یک «بوی کد» (code smell) است. |
راهنمای کلیدی
از خود بپرسید: «اگر این بررسی شکست بخورد چه اتفاقی باید بیفتد؟»
- اگر یک مسیر جایگزین قانونی وجود دارد (مثلاً، اگر کاربر احراز هویت نشده است، دکمه ورود را نشان بده)، از یک تایپ گارد با یک بلوک
if/else
استفاده کنید. - اگر یک بررسی ناموفق به این معنی است که برنامه شما در وضعیت نامعتبری قرار دارد و نمیتواند با خیال راحت ادامه دهد، از یک تابع assertion استفاده کنید.
- اگر در حال نادیده گرفتن کامپایلر بدون بررسی زمان اجرا هستید، از تبدیل نوع استفاده میکنید. بسیار مراقب باشید.
الگوهای پیشرفته و بهترین شیوهها
۱. یک کتابخانه مرکزی Assertion ایجاد کنید
توابع assertion را در سراسر کدبیس خود پراکنده نکنید. آنها را در یک فایل ابزار اختصاصی، مانند src/utils/assertions.ts
، متمرکز کنید. این کار قابلیت استفاده مجدد، ثبات را ترویج میدهد و پیدا کردن و تست کردن منطق اعتبارسنجی شما را آسان میکند.
// src/utils/assertions.ts
export function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
export function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
assert(value !== null && value !== undefined, 'This value must be defined.');
}
export function assertIsString(value: unknown): asserts value is string {
assert(typeof value === 'string', 'This value must be a string.');
}
// ... و غیره.
۲. خطاهای معنادار پرتاب کنید
پیام خطای یک assertion ناموفق، اولین سرنخ شما در هنگام اشکالزدایی است. آن را ارزشمند کنید! یک پیام عمومی مانند «Assertion failed» مفید نیست. به جای آن، زمینه را فراهم کنید:
- چه چیزی در حال بررسی بود؟
- مقدار/نوع مورد انتظار چه بود؟
- مقدار/نوع واقعی دریافت شده چه بود؟ (مراقب باشید دادههای حساس را لاگ نکنید).
function assertIsUser(data: unknown): asserts data is User {
if (!isUser(data)) {
// بد: throw new Error('Invalid data');
// خوب:
throw new TypeError(`Expected data to be a User object, but received ${JSON.stringify(data)}`);
}
}
۳. به عملکرد توجه داشته باشید
توابع assertion بررسیهای زمان اجرا هستند، به این معنی که چرخههای CPU را مصرف میکنند. این در مرزهای برنامه شما (ورودی API، بارگذاری پیکربندی) کاملاً قابل قبول و مطلوب است. با این حال، از قرار دادن assertionهای پیچیده در مسیرهای کد حیاتی از نظر عملکرد، مانند یک حلقه فشرده که هزاران بار در ثانیه اجرا میشود، خودداری کنید. از آنها در جایی استفاده کنید که هزینه بررسی در مقایسه با عملیات انجام شده (مانند یک درخواست شبکه) ناچیز باشد.
نتیجهگیری: نوشتن کد با اطمینان
توابع assertion در TypeScript چیزی بیش از یک ویژگی خاص نیستند؛ آنها ابزاری اساسی برای نوشتن برنامههای قوی و در سطح پروداکشن هستند. آنها به شما قدرت میدهند تا بر شکاف حیاتی بین تئوری زمان کامپایل و واقعیت زمان اجرا پل بزنید.
با اتخاذ توابع assertion، شما میتوانید:
- اعمال ثباتها (Invariants): شرایطی را که باید درست باشند به طور رسمی اعلام کنید و فرضیات کد خود را صریح سازید.
- سریع و با صدای بلند شکست بخورید: مشکلات یکپارچگی داده را در مبدأ شناسایی کنید و از ایجاد باگهای نامحسوس و دشوار برای اشکالزدایی در مراحل بعد جلوگیری کنید.
- بهبود خوانایی کد: بررسیهای تودرتوی
if
و تبدیلهای نوع را حذف کنید، که منجر به منطق تجاری تمیزتر، خطیتر و خود-مستند میشود. - افزایش اطمینان: کدی بنویسید با این اطمینان که انواع شما فقط پیشنهادهایی برای کامپایلر نیستند، بلکه هنگام اجرای کد به طور فعال اعمال میشوند.
دفعه بعد که دادهای را از یک API واکشی میکنید، یک فایل پیکربندی میخوانید یا ورودی کاربر را پردازش میکنید، فقط نوع را تبدیل نکنید و به بهترینها امیدوار باشید. آن را Assert کنید. یک دروازه ایمنی در لبه سیستم خود بسازید. خود آیندهتان - و تیمتان - از شما برای کد قوی، قابل پیشبینی و مقاومی که نوشتهاید، سپاسگزار خواهند بود.