دليل شامل لدوال التأكيد في TypeScript. تعلم كيفية سد الفجوة بين وقت التصريف ووقت التشغيل، والتحقق من صحة البيانات، وكتابة كود أكثر أمانًا وقوة مع أمثلة عملية.
دوال التأكيد في TypeScript: الدليل الشامل لسلامة الأنواع وقت التشغيل
في عالم تطوير الويب، غالبًا ما يكون العقد بين توقعات الكود الخاص بك وواقع البيانات التي يتلقاها هشًا. لقد أحدثت TypeScript ثورة في طريقة كتابتنا لـ JavaScript من خلال توفير نظام أنواع ثابت قوي، مما يمنع عددًا لا يحصى من الأخطاء قبل وصولها إلى بيئة الإنتاج. ومع ذلك، فإن شبكة الأمان هذه توجد بشكل أساسي في وقت التصريف (compile-time). ماذا يحدث عندما يتلقى تطبيقك المكتوب بأنواع جميلة بيانات فوضوية وغير متوقعة من العالم الخارجي في وقت التشغيل (runtime)؟ هنا تصبح دوال التأكيد في TypeScript أداة لا غنى عنها لبناء تطبيقات قوية حقًا.
سيأخذك هذا الدليل الشامل في رحلة عميقة إلى دوال التأكيد. سنستكشف سبب ضرورتها، وكيفية بنائها من الصفر، وكيفية تطبيقها على سيناريوهات شائعة في العالم الحقيقي. بحلول نهاية هذا الدليل، ستكون مجهزًا لكتابة كود ليس فقط آمنًا من ناحية الأنواع في وقت التصريف، بل وأيضًا مرنًا ومتوقعًا في وقت التشغيل.
الانقسام الكبير: وقت التصريف مقابل وقت التشغيل
لتقدير أهمية دوال التأكيد حقًا، يجب علينا أولاً فهم التحدي الأساسي الذي تحله: الفجوة بين عالم TypeScript في وقت التصريف وعالم JavaScript في وقت التشغيل.
جنة وقت التصريف في TypeScript
عندما تكتب كود TypeScript، فأنت تعمل في جنة المطورين. يعمل مترجم TypeScript (tsc
) كمساعد يقظ، حيث يحلل الكود الخاص بك مقابل الأنواع التي حددتها. إنه يتحقق من:
- تمرير أنواع غير صحيحة إلى الدوال.
- الوصول إلى خصائص غير موجودة في كائن ما.
- استدعاء متغير قد يكون
null
أوundefined
.
تحدث هذه العملية قبل تنفيذ الكود الخاص بك. الناتج النهائي هو JavaScript عادي، مجرد من جميع تعليقات الأنواع. فكر في TypeScript كمخطط معماري مفصل لمبنى. يضمن أن جميع الخطط سليمة، والقياسات صحيحة، والسلامة الهيكلية مضمونة على الورق.
واقع وقت التشغيل في JavaScript
بمجرد ترجمة TypeScript إلى JavaScript وتشغيله في متصفح أو بيئة Node.js، تختفي الأنواع الثابتة. يعمل الكود الخاص بك الآن في عالم وقت التشغيل الديناميكي وغير المتوقع. يجب أن يتعامل مع بيانات من مصادر لا يمكنه التحكم فيها، مثل:
- استجابات واجهة برمجة التطبيقات (API Responses): قد تقوم خدمة خلفية بتغيير بنية بياناتها بشكل غير متوقع.
- إدخال المستخدم (User Input): دائمًا ما تُعامل البيانات من نماذج HTML كنصوص، بغض النظر عن نوع الإدخال.
- التخزين المحلي (Local Storage): البيانات المسترجعة من
localStorage
تكون دائمًا نصية وتحتاج إلى تحليل. - متغيرات البيئة (Environment Variables): غالبًا ما تكون هذه نصوصًا وقد تكون مفقودة تمامًا.
لاستخدام تشبيهنا، وقت التشغيل هو موقع البناء. كان المخطط مثاليًا، لكن المواد التي تم تسليمها (البيانات) قد تكون بالحجم الخاطئ، أو النوع الخاطئ، أو ببساطة مفقودة. إذا حاولت البناء بهذه المواد المعيبة، فسوف ينهار هيكلك. هنا تحدث أخطاء وقت التشغيل، والتي غالبًا ما تؤدي إلى تعطل البرنامج وأخطاء مثل "Cannot read properties of undefined".
إدخال دوال التأكيد: سد الفجوة
إذًا، كيف نفرض مخطط TypeScript الخاص بنا على مواد وقت التشغيل غير المتوقعة؟ نحن بحاجة إلى آلية يمكنها فحص البيانات *عند وصولها* وتأكيد مطابقتها لتوقعاتنا. هذا هو بالضبط ما تفعله دوال التأكيد.
ما هي دالة التأكيد؟
دالة التأكيد هي نوع خاص من الدوال في TypeScript تخدم غرضين حاسمين:
- التحقق وقت التشغيل: تقوم بإجراء تحقق من صحة قيمة أو شرط. إذا فشل التحقق، فإنها تطلق خطأ (throw an error)، مما يوقف تنفيذ مسار الكود هذا فورًا. هذا يمنع البيانات غير الصالحة من الانتشار أكثر في تطبيقك.
- تضييق النوع وقت التصريف: إذا نجح التحقق (أي لم يتم إطلاق خطأ)، فإنها تشير إلى مترجم TypeScript أن نوع القيمة أصبح الآن أكثر تحديدًا. يثق المترجم بهذا التأكيد ويسمح لك باستخدام القيمة بالنوع المؤكد لبقية نطاقها.
يكمن السحر في توقيع الدالة، الذي يستخدم الكلمة المفتاحية asserts
. هناك شكلان أساسيان:
asserts condition [is type]
: هذا الشكل يؤكد أنcondition
معين هو `truthy`. يمكنك اختياريًا تضمينis type
(مُسند نوع) لتضييق نوع متغير أيضًا.asserts this is type
: يستخدم هذا داخل دوال الأصناف (class methods) لتأكيد نوع سياقthis
.
الفكرة الرئيسية هي سلوك "الإطلاق عند الفشل". على عكس فحص if
البسيط، يعلن التأكيد: "يجب أن يكون هذا الشرط صحيحًا لكي يستمر البرنامج. إذا لم يكن كذلك، فهذه حالة استثنائية، ويجب أن نتوقف فورًا."
بناء أول دالة تأكيد لك: مثال عملي
لنبدأ بواحدة من أكثر المشاكل شيوعًا في JavaScript و TypeScript: التعامل مع القيم التي قد تكون null
أو undefined
.
المشكلة: القيم الفارغة غير المرغوب فيها
تخيل دالة تأخذ كائن مستخدم اختياري وتريد تسجيل اسم المستخدم. ستحذرنا فحوصات 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` هو `undefined` خطأ لا يمكن إصلاحه في هذا السياق؟ لا نريد أن تستمر الدالة بصمت. نريدها أن تفشل بصوت عالٍ. هذا يؤدي إلى شروط حماية متكررة.
الحل: دالة تأكيد `assertIsDefined`
لنقم بإنشاء دالة تأكيد قابلة لإعادة الاستخدام للتعامل مع هذا النمط بأناقة.
// دالة التأكيد القابلة لإعادة الاستخدام
function assertIsDefined<T>(value: T, message: string = "القيمة غير معرفة"): 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, "يجب توفير كائن المستخدم لتسجيل الاسم.");
// لا يوجد خطأ! يعرف 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); // يطلق خطأ: "يجب توفير كائن المستخدم لتسجيل الاسم."
} catch (error) {
console.error(error.message);
}
تفكيك توقيع التأكيد
لنفكك التوقيع: asserts value is NonNullable<T>
asserts
: هذه هي الكلمة المفتاحية الخاصة في TypeScript التي تحول هذه الدالة إلى دالة تأكيد.value
: هذا يشير إلى المعلمة الأولى للدالة (في حالتنا، المتغير المسمى `value`). يخبر TypeScript أي متغير يجب تضييق نوعه.is NonNullable<T>
: هذا هو مُسند النوع (type predicate). يخبر المترجم أنه إذا لم تطلق الدالة خطأ، فإن نوع `value` هو الآنNonNullable<T>
. النوع المساعدNonNullable
في TypeScript يزيلnull
وundefined
من النوع.
حالات استخدام عملية لدوال التأكيد
الآن بعد أن فهمنا الأساسيات، دعنا نستكشف كيفية تطبيق دوال التأكيد لحل المشكلات الشائعة في العالم الحقيقي. تكون أكثر قوة عند حدود تطبيقك، حيث تدخل البيانات الخارجية غير المحددة النوع إلى نظامك.
حالة الاستخدام 1: التحقق من صحة استجابات 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'
);
}
// دالة التأكيد الجديدة الخاصة بنا
function assertIsUser(data: unknown): asserts data is User {
if (!isUser(data)) {
throw new TypeError('تم استلام بيانات مستخدم غير صالحة من API.');
}
}
async function fetchAndProcessUser(userId: number) {
const response = await fetch(`https://api.example.com/users/${userId}`);
const data: unknown = await response.json();
// تأكد من شكل البيانات عند الحدود
assertIsUser(data);
// من هذه النقطة فصاعدًا، 'data' مصنفة بأمان كـ 'User'.
// لا حاجة بعد الآن لفحوصات 'if' أو تحويل الأنواع!
console.log(`معالجة المستخدم: ${data.name.toUpperCase()} (${data.email})`);
}
fetchAndProcessUser(1);
لماذا هذا قوي: من خلال استدعاء `assertIsUser(data)` مباشرة بعد تلقي الاستجابة، نقوم بإنشاء "بوابة أمان". أي كود يتبع يمكنه بثقة التعامل مع `data` كـ `User`. هذا يفصل منطق التحقق من صحة البيانات عن منطق العمل، مما يؤدي إلى كود أنظف وأكثر قابلية للقراءة.
حالة الاستخدام 2: ضمان وجود متغيرات البيئة
تعتمد التطبيقات من جانب الخادم (على سبيل المثال، في 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(`خطأ فادح: متغير البيئة ${key} غير معين.`);
}
return value;
}
// نسخة أكثر قوة باستخدام التأكيدات
function assertEnvVar(key: string): asserts key is keyof NodeJS.ProcessEnv {
if (process.env[key] === undefined) {
throw new Error(`خطأ فادح: متغير البيئة ${key} غير معين.`);
}
}
// في نقطة الدخول لتطبيقك، على سبيل المثال، '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:', apiKey.length);
console.log('الاتصال بقاعدة البيانات:', dbUrl.toLowerCase());
// ... بقية منطق بدء تشغيل الخادم
}
startServer();
لماذا هذا قوي: يسمى هذا النمط "الفشل السريع" (fail-fast). أنت تتحقق من جميع التكوينات الحرجة مرة واحدة في بداية دورة حياة تطبيقك. إذا كانت هناك مشكلة، فإنه يفشل على الفور مع خطأ وصفي، وهو أسهل بكثير في التصحيح من تعطل غامض يحدث لاحقًا عند استخدام المتغير المفقود أخيرًا.
حالة الاستخدام 3: العمل مع DOM
عندما تستعلم عن DOM، على سبيل المثال باستخدام `document.querySelector`، تكون النتيجة `Element | null`. إذا كنت متأكدًا من وجود عنصر ما (على سبيل المثال، `div` الجذر الرئيسي للتطبيق)، فإن التحقق المستمر من `null` يمكن أن يكون مرهقًا.
السيناريو
لدينا ملف HTML يحتوي على `
`، ويحتاج السكربت الخاص بنا إلى إرفاق محتوى به. نحن نعلم أنه موجود.
// إعادة استخدام تأكيدنا العام من قبل
function assertIsDefined<T>(value: T, message: string = "القيمة غير معرفة"): asserts value is NonNullable<T> {
if (value === undefined || value === null) {
throw new Error(message);
}
}
// تأكيد أكثر تحديدًا لعناصر DOM
function assertQuerySelector<T extends Element>(selector: string, constructor?: new () => T): T {
const element = document.querySelector(selector);
assertIsDefined(element, `خطأ فادح: لم يتم العثور على عنصر بالمحدد '${selector}' في DOM.`);
// اختياري: تحقق مما إذا كان من النوع الصحيح للعنصر
if (constructor && !(element instanceof constructor)) {
throw new TypeError(`العنصر '${selector}' ليس من نوع ${constructor.name}`);
}
return element as T;
}
// الاستخدام
const appRoot = document.querySelector('#app-root');
assertIsDefined(appRoot, 'تعذر العثور على عنصر جذر التطبيق الرئيسي.');
// بعد التأكيد، appRoot من نوع 'Element'، وليس 'Element | null'.
appRoot.innerHTML = 'مرحباً بالعالم!
';
// استخدام المساعد الأكثر تحديدًا
const submitButton = assertQuerySelector<HTMLButtonElement>('#submit-btn', HTMLButtonElement);
// 'submitButton' الآن مصنف بشكل صحيح كـ HTMLButtonElement
submitButton.disabled = true;
لماذا هذا قوي: يسمح لك بالتعبير عن ثابت (invariant) — شرط تعرف أنه صحيح — حول بيئتك. يزيل كود التحقق من `null` المزعج ويوثق بوضوح اعتماد السكربت على بنية DOM محددة. إذا تغيرت البنية، تحصل على خطأ فوري وواضح.
دوال التأكيد مقابل البدائل
من الأهمية بمكان معرفة متى تستخدم دالة تأكيد مقابل تقنيات تضييق النوع الأخرى مثل حراس الأنواع أو تحويل الأنواع.
التقنية | الصيغة | السلوك عند الفشل | الأفضل لـ |
---|---|---|---|
حراس الأنواع (Type Guards) | value is Type |
يعيد false |
التحكم في التدفق (if/else ). عندما يكون هناك مسار كود بديل صالح للحالة "غير السعيدة". على سبيل المثال، "إذا كان نصًا، فقم بمعالجته؛ وإلا، استخدم قيمة افتراضية." |
دوال التأكيد (Assertion Functions) | asserts value is Type |
يطلق Error |
فرض الثوابت. عندما *يجب* أن يكون الشرط صحيحًا لكي يستمر البرنامج بشكل صحيح. المسار "غير السعيد" هو خطأ لا يمكن إصلاحه. على سبيل المثال، "استجابة API *يجب* أن تكون كائن User." |
تحويل الأنواع (Type Casting) | value as Type |
لا يوجد تأثير في وقت التشغيل | الحالات النادرة التي تعرف فيها، كمطور، أكثر من المترجم وقد قمت بالفعل بالفحوصات اللازمة. لا يقدم أي أمان في وقت التشغيل ويجب استخدامه باعتدال. الإفراط في استخدامه هو "رائحة كود" (code smell). |
إرشادات رئيسية
اسأل نفسك: "ماذا يجب أن يحدث إذا فشل هذا الفحص؟"
- إذا كان هناك مسار بديل شرعي (على سبيل المثال، إظهار زر تسجيل الدخول إذا لم يكن المستخدم مصادقًا عليه)، استخدم حارس نوع مع كتلة
if/else
. - إذا كان الفحص الفاشل يعني أن برنامجك في حالة غير صالحة ولا يمكنه المتابعة بأمان، فاستخدم دالة تأكيد.
- إذا كنت تتجاوز المترجم بدون فحص في وقت التشغيل، فأنت تستخدم تحويل نوع. كن حذرًا جدًا.
الأنماط المتقدمة وأفضل الممارسات
1. إنشاء مكتبة تأكيد مركزية
لا تنثر دوال التأكيد في جميع أنحاء قاعدة الكود الخاصة بك. قم بمركزتها في ملف أدوات مخصص، مثل 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, 'يجب تحديد هذه القيمة.');
}
export function assertIsString(value: unknown): asserts value is string {
assert(typeof value === 'string', 'يجب أن تكون هذه القيمة نصًا.');
}
// ... وهكذا.
2. إطلاق أخطاء ذات معنى
رسالة الخطأ من تأكيد فاشل هي دليلك الأول أثناء تصحيح الأخطاء. اجعلها مهمة! رسالة عامة مثل "فشل التأكيد" ليست مفيدة. بدلاً من ذلك، قدم سياقًا:
- ماذا كان يتم فحصه؟
- ماذا كانت القيمة/النوع المتوقع؟
- ماذا كانت القيمة/النوع الفعلي المستلم؟ (كن حذرًا من تسجيل البيانات الحساسة).
function assertIsUser(data: unknown): asserts data is User {
if (!isUser(data)) {
// سيء: throw new Error('بيانات غير صالحة');
// جيد:
throw new TypeError(`كان من المتوقع أن تكون البيانات كائن User، ولكن تم استلام ${JSON.stringify(data)}`);
}
}
3. كن واعيًا بالأداء
دوال التأكيد هي فحوصات وقت التشغيل، مما يعني أنها تستهلك دورات وحدة المعالجة المركزية. هذا مقبول تمامًا ومرغوب فيه عند حدود تطبيقك (دخول API، تحميل التكوين). ومع ذلك، تجنب وضع تأكيدات معقدة داخل مسارات الكود الحرجة من حيث الأداء، مثل حلقة ضيقة تعمل آلاف المرات في الثانية. استخدمها حيث تكون تكلفة الفحص ضئيلة مقارنة بالعملية التي يتم تنفيذها (مثل طلب الشبكة).
الخاتمة: كتابة الكود بثقة
دوال التأكيد في TypeScript هي أكثر من مجرد ميزة متخصصة؛ إنها أداة أساسية لكتابة تطبيقات قوية وجاهزة للإنتاج. إنها تمكنك من سد الفجوة الحرجة بين نظرية وقت التصريف وواقع وقت التشغيل.
من خلال اعتماد دوال التأكيد، يمكنك:
- فرض الثوابت: الإعلان رسميًا عن الشروط التي يجب أن تكون صحيحة، مما يجعل افتراضات الكود الخاص بك صريحة.
- الفشل السريع والصاخب: اكتشاف مشكلات سلامة البيانات من المصدر، ومنعها من التسبب في أخطاء دقيقة وصعبة التصحيح لاحقًا.
- تحسين وضوح الكود: إزالة فحوصات
if
المتداخلة وتحويلات الأنواع، مما ينتج عنه منطق عمل أنظف وأكثر خطية وتوثيقًا ذاتيًا. - زيادة الثقة: كتابة الكود مع التأكيد على أن أنواعك ليست مجرد اقتراحات للمترجم ولكن يتم فرضها بنشاط عند تنفيذ الكود.
في المرة القادمة التي تجلب فيها بيانات من واجهة برمجة تطبيقات، أو تقرأ ملف تكوين، أو تعالج إدخال المستخدم، لا تقم فقط بتحويل النوع وتأمل في الأفضل. أكّد عليه. قم ببناء بوابة أمان على حافة نظامك. سيشكرك مستقبلك — وفريقك — على الكود القوي والمتوقع والمرن الذي كتبته.