لعنة المعاملات المفقودة في TypeScript
\nهل سبق لك أن كتبت دالة بعشرة معاملات اختيارية، ثم حاولت تمرير الخامس والعاشر فقط؟ وفجأة حصلت على خطأ في الترجمة، أو الأسوأ من ذلك، خطأ منطقي صامت؟ مألوف؟
\nعلى Stack Overflow، جمع هذا السؤال آلاف المشاهدات. مطورو TypeScript يتعثرون باستمرار في نفس المشكلة: كيف تمرر معاملًا اختياريًا واحدًا، متخطيًا آخر؟ لأن الصيغة القياسية لا تسمح بذلك – يتم تمرير المعاملات حسب الموضع وليس حسب الاسم.
\nاليوم سنحل هذه المشكلة مرة واحدة وإلى الأبد. سأريكم 4 طرق عملية لحل المشكلة: من إعادة الهيكلة البسيطة إلى التقنيات المتقدمة مع كائنات partial. هيا بنا!
\n\nلماذا هذه مشكلة أساسًا؟ اللعنة الموضعية
\nفي TypeScript (كما في JavaScript)، يتم تمرير معاملات الدالة بالترتيب. إذا كان لديك دالة:
\nfunction createUser(\n name: string,\n age?: number,\n email?: string,\n isAdmin?: boolean\n) { /* ... */ }\nلا يمكنك استدعاؤها هكذا: createUser('John', , 'john@mail.com'). تخطي المعامل يترك فراغًا لا يستطيع TypeScript التعامل معه. سيتعين عليك تمرير undefined أو null، مما يلوث الكود والمنطق.
لكن هناك خبر سار: يمكننا تجاوز هذا القيد بعدة طرق أنيقة. دعنا نستعرض كلًا منها.
\n\nالطريقة 1: كائن المعاملات (الأكثر صحة)
\nهذا هو النمط الموصى به في المشاريع الجادة. بدلاً من مجموعة من المعاملات الموضعية، نستخدم كائنًا واحدًا بحقول اختيارية:
\ninterface UserOptions {\n name: string;\n age?: number;\n email?: string;\n isAdmin?: boolean;\n}\n\nfunction createUser(options: UserOptions): void {\n const { name, age, email, isAdmin } = options;\n console.log(`تم إنشاء المستخدم ${name}، العمر: ${age ?? 'غير محدد'}`);\n}\n\n// الآن يمكنك تمرير ما تحتاجه فقط!\ncreateUser({ name: 'آنا', email: 'anna@mail.com' });\ncreateUser({ name: 'بيتر', isAdmin: true });\n\nالمزايا واضحة:
\n- \n
- لا يهم عدد المعاملات – نمرر فقط ما نحتاجه \n
- الكود موثق ذاتيًا: يظهر فورًا ما هو المسؤول عن ماذا \n
- سهولة إضافة حقول جديدة دون تغيير التوقيع \n
هذا الأسلوب يحل 90% من مشاكل المعاملات الاختيارية. لكن إذا كنت تعمل مع نظام قديم أو لا يمكنك تغيير API – تابع القراءة.
\n\nالطريقة 2: التحميل الزائد للدوال (function overloading)
\nيسمح TypeScript بتعريف عدة تواقيع لدالة واحدة. هذا مفيد عندما يكون لديك مجموعات محددة من المعاملات:
\n// تعريف التحميلات الزائدة\nfunction getData(url: string): Promise<any>;\nfunction getData(url: string, timeout: number): Promise<any>;\nfunction getData(url: string, timeout: number, retries: number): Promise<any>;\n\n// التنفيذ (يجب أن يكون الأكثر عمومية)\nfunction getData(\n url: string,\n timeout?: number,\n retries?: number\n): Promise<any> {\n // المنطق\n return fetch(url, { signal: AbortSignal.timeout(timeout ?? 5000) });\n}\n\n// الآن يمكنك الاستدعاء هكذا:\ngetData('/api/users'); // timeout = 5000 (افتراضي)\ngetData('/api/users', 3000); // timeout = 3000\n// لكن هكذا – لا يمكن (تخطي timeout غير مسموح بدون تحميل زائد):\n// getData('/api/users', undefined, 2); // سيء\n\nالعيب: التحميل الزائد لا يحل مشكلة تخطي المعاملات الوسطى. إنها تعطي فقط مجموعات صارمة. للمرونة، استخدم الطريقة الأولى.
\n\nالطريقة 3: معاملات rest + التفكيك (الغريبة)
\nإذا كنت تريد الحفاظ على الموضعية ولكن إضافة مرونة، يمكنك استخدام tuples مع rest:
\ntype OptionalParams = [number, string?, boolean?];\n\nfunction processData(\n required: string,\n ...opt