من استخدام any عشوائياً إلى تجاهل strictNullChecks، هذه الأخطاء في TypeScript تكلف فرق التطوير ساعات من تصحيح الأخطاء. اكتشف كيف تتجنبها من تجارب حقيقية في شركات تقنية كبرى.
في أحد المشاريع الكبيرة التي عملت عليها، كان لدينا سيرفر Node.js مكتوب بـ TypeScript يتعامل مع ٥٠ ألف طلب في الدقيقة. فجأة، بدأ السيرفر يعلق بشكل عشوائي دون أي خطأ واضح في السجلات. بعد ٤٨ ساعة من التصحيح، اكتشفنا أن المشكلة كانت في استخدام نوع union غير آمن مع null. هذا الخطأ البسيط تسبب في تسرب ذاكرة بطيء أدى إلى توقف السيرفر تحت الحمل. الحقيقة هي أن TypeScript لا يحميك من الأخطاء المنطقية، بل يحميك فقط من الأخطاء النوعية إذا استخدمته بشكل صحيح. المشكلة الأكبر أن معظم المطورين يستخدمون TypeScript كطبقة تجميلية فوق JavaScript بدلاً من الاستفادة الحقيقية من نظام الأنواع القوي.
في هذا المقال، سأشارك معك الأخطاء الشائعة التي أراها مراراً وتكراراً في مشاريع TypeScript، سواء في الشركات الناشئة أو المؤسسات الكبيرة. لن نتحدث عن الأخطاء السطحية مثل نسيان الفاصلة المنقوطة، بل عن الأخطاء العميقة التي تؤثر على أداء التطبيق واستقراره وصيانته على المدى الطويل. سأشرح لك ليس فقط كيف تتجنب هذه الأخطاء، بل أيضاً ماذا يحدث خلف الكواليس في الذاكرة والمعالج عندما تقع فيها.
عندما بدأت TypeScript في الانتشار، كان أول شيء يفعله المطورون هو كتابة // @ts-ignore أو استخدام any في كل مكان. المشكلة أن any ليس مجرد نوع، بل هو ثقب أسود يمتص كل فوائد TypeScript. عندما تستخدم any، أنت تخبر المحرك: "لا تهتم، هذا النوع غير معروف، افعل ما تريد". هذا يعني أنك تفقد التحقق من الأنواع، الاقتراحات الذكية في المحرر، والقدرة على اكتشاف الأخطاء في وقت التحويل بدلاً من وقت التشغيل.
في مشروع لشركة fintech، كان لدينا دالة تحسب الفائدة المركبة تأخذ كائن options يحتوي على معدل الفائدة وعدد السنوات. المطور السابق استخدم any لهذا الكائن، مما أدى إلى خطأ حسابي كبير عندما تم تمرير سلسلة نصية بدلاً من رقم. الخطأ لم يظهر إلا بعد شهرين من الإنتاج عندما اكتشف العميل أن حسابه ناقص ٢٠٠ دولار. الحل؟ استخدمنا نوع مخصص بدلاً من any:
type CompoundInterestOpti {
principal: number;
rate: number;
years: number;
compoundFrequency?: number;
};
function calculateCompoundInterest(options: CompoundInterestOptions): number {
const { principal, rate, years, compoundFrequency = 1 } = options;
return principal * Math.pow(1 + (rate / 100) / compoundFrequency, compoundFrequency * years);
}
// بدلاً من
// function calculateCompoundInterest(options: any): number {
// return options.principal * Math.pow(1 + options.rate / 100, options.years);
// }لاحظ كيف أن النوع المخصص يجبرك على تمرير القيم الصحيحة ويظهر أخطاء في وقت التحويل إذا حاولت تمرير نوع خاطئ. هذا النوع من التحقق يوفر ساعات من تصحيح الأخطاء لاحقاً. القاعدة الذهبية: إذا وجدت نفسك تستخدم any، توقف واسأل نفسك لماذا لا يمكنك استخدام نوع محدد بدلاً منه. حتى unknown أفضل من any لأنه يجبرك على التحقق من النوع قبل استخدامه.
في JavaScript، كل شيء يمكن أن يكون null أو undefined. TypeScript يحاول حل هذه المشكلة من خلال خيار strictNullChecks، لكن الكثير من المطورين يطفئونه أو يتجاهلونه. النتيجة؟ أخطاء في وقت التشغيل مثل Cannot read property 'x' of undefined. في مشروع لشركة e-commerce، كان لدينا دالة تحسب الخصم على المنتج:
// بدون strictNullChecks
function calculateDiscount(price: number, discount: number): number {
return price - (price * discount);
}
// في مكان آخر
const product = getProductFromAPI();
const finalPrice = calculateDiscount(product.price, product.discount); // خطأ إذا كان product أو discount غير معرفالمشكلة أن هذا الكود يعمل بشكل جيد في التطوير، لكنه يفشل في الإنتاج عندما يكون أحد الحقول غير معرف. مع تفعيل strictNullChecks، ستظهر الأخطاء في وقت التحويل بدلاً من وقت التشغيل. الحل هو استخدام أنواع union مع null أو undefined والتحقق منها بشكل صحيح:
type Product = {
price: number;
discount?: number | null; // خصم اختياري قد يكون null
};
function calculateDiscount(price: number, discount: number | null | undefined): number {
if (discount == null) return price; // التحقق من null و undefined
return price - (price * discount);
}
// أو باستخدام عامل الاختيار الاختياري
const finalPrice = calculateDiscount(product.price, product.discount ?? 0);التحويلات غير الآمنة مثل as هي أيضاً مشكلة شائعة. عندما تستخدم as، أنت تخبر TypeScript "ثق بي، أنا أعرف ما أفعله". هذا غالباً ما يؤدي إلى أخطاء في وقت التشغيل. بدلاً من ذلك، استخدم النوع المحدد أو التحقق من النوع باستخدام typeof أو instanceof:
// سيء: تحويل غير آمن
const data = response.data as User;
// جيد: التحقق من النوع
if (isUser(response.data)) {
const user: User = response.data;
}
function isUser(data: any): data is User {
return data && typeof data.id === 'string' && typeof data.name === 'string';
}الأنواع العامة في TypeScript هي أداة قوية، لكنها غالباً ما تُستخدم بشكل مفرط. عندما ترى دالة مثل هذه، فاعلم أن هناك مشكلة:
function processData<T>(data: T): T {
// ماذا نفعل هنا؟ لا نعرف شيئاً عن T
return data;
}هذه الدالة لا تقدم أي قيمة لأن النوع العام T لا يوفر أي معلومات عن البيانات. في مشروع لشركة SaaS، كان لدينا خدمة تتعامل مع البيانات من مصادر مختلفة، وكان المطور يستخدم generics في كل مكان دون الحاجة الحقيقية إليها. النتيجة؟ كود معقد يصعب فهمه وصيانته. بدلاً من ذلك، استخدم أنواعاً محددة أو قيوداً على الأنواع العامة:
// سيء: نوع عام بدون قيود
function mergeObjects<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
// جيد: قيود على الأنواع العامة
function mergeObjects<T extends object, U extends object>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
// أفضل: استخدام أنواع محددة إذا كانت البيانات معروفة
function mergeUserData(user: User, preferences: UserPreferences): UserWithPreferences {
return { ...user, ...preferences };
}القاعدة هنا هي: استخدم الأنواع العامة فقط عندما تحتاج إلى كتابة كود يعمل مع أنواع متعددة ولكن بنفس السلوك. إذا كنت تعرف النوع المحدد، استخدمه بدلاً من النوع العام. هذا يجعل الكود أكثر قابلية للقراءة ويسهل على المحرر تقديم اقتراحات ذكية.
في JavaScript، الدوال يمكنها دائماً إرجاع أي شيء، وهذا ما يجعل TypeScript يسمح لك بتجاهل أنواع الإرجاع. لكن هذا خطأ شائع يؤدي إلى كود غير متوقع. عندما لا تحدد نوع الإرجاع، TypeScript يحاول استنتاجه، وهذا قد يؤدي إلى أنواع غير دقيقة خاصة مع الدوال المعقدة.
في مشروع لشركة healthcare، كان لدينا دالة تحسب مؤشر كتلة الجسم (BMI) وكانت تُرجع سلسلة نصية في بعض الحالات ورقماً في حالات أخرى. المطور لم يحدد نوع الإرجاع، مما أدى إلى خطأ في واجهة المستخدم عندما تم تمرير النتيجة إلى دالة تتوقع رقماً. الحل هو دائماً تحديد نوع الإرجاع بوضوح:
// سيء: نوع الإرجاع غير محدد
function calculateBMI(weight: number, height: number) {
const bmi = weight / (height * height);
if (bmi < 18.5) return 'Underweight';
if (bmi < 25) return 'Normal';
if (bmi < 30) return 'Overweight';
return 'Obese';
}
// جيد: نوع الإرجاع محدد
function calculateBMI(weight: number, height: number): number {
return weight / (height * height);
}
// أو إذا كنت تريد إرجاع سلسلة نصية
function getBMICategory(bmi: number): string {
if (bmi < 18.5) return 'Underweight';
if (bmi < 25) return 'Normal';
if (bmi < 30) return 'Overweight';
return 'Obese';
}تحديد أنواع الإرجاع يجبرك على التفكير في ما ترجعه الدالة ويجعل الكود أكثر قابلية للتنبؤ. كما أنه يساعد في اكتشاف الأخطاء مبكراً. مثلاً، إذا كتبت دالة من المفترض أن ترجع Promise ولكن نسيت await، سيظهر خطأ في وقت التحويل إذا كان نوع الإرجاع محدداً:
async function fetchUser(id: string): Promise<User> {
const resp await fetch(`/api/users/${id}`);
return response.json(); // صحيح
}
// إذا نسيت await
async function fetchUser(id: string): Promise<User> {
const response = fetch(`/api/users/${id}`); // خطأ: Promise<Promise<User>>
return response.json();
}النوع Union في TypeScript يسمح لك بتعريف متغير يمكنه أن يكون واحداً من عدة أنواع. هذا مفيد في بعض الحالات، لكنه غالباً ما يُساء استخدامه. عندما ترى نوع union مع أكثر من نوعين أو ثلاثة، فاعلم أن هناك فرصة لتحسين الكود باستخدام نوع مخصص.
في مشروع لشركة logistics، كان لدينا دالة تتعامل مع شحنات مختلفة وكانت تأخذ نوع union مثل هذا:
function processShipment(shipment: {
id: string;
type: 'standard' | 'express' | 'overnight';
weight: number;
dimensions?: { width: number; height: number; depth: number };
fragile?: boolean;
international?: boolean;
customsInfo?: {
value: number;
description: string;
countryOfOrigin: string;
};
} | {
id: string;
type: 'digital';
fileSize: number;
fileType: string;
}) {
// منطق معالجة الشحنة
}هذا النوع صعب القراءة والفهم، كما أنه يجعل الكود الذي يتعامل معه معقداً. الحل هو استخدام أنواع مخصصة مع union للنوع الرئيسي:
type PhysicalShipment = {
id: string;
type: 'standard' | 'express' | 'overnight';
weight: number;
dimensions?: { width: number; height: number; depth: number };
fragile?: boolean;
international?: boolean;
customsInfo?: {
value: number;
description: string;
countryOfOrigin: string;
};
};
type DigitalShipment = {
id: string;
type: 'digital';
fileSize: number;
fileType: string;
};
type Shipment = PhysicalShipment | DigitalShipment;
function processShipment(shipment: Shipment) {
if (shipment.type === 'digital') {
// التعامل مع الشحنة الرقمية
} else {
// التعامل مع الشحنة المادية
}
}هذا الأسلوب يجعل الكود أكثر قابلية للصيانة ويسهل إضافة أنواع جديدة في المستقبل. كما أنه يسمح لك باستخدام التحقق من النوع في وقت التشغيل بسهولة أكبر باستخدام خاصية discriminated union مثل type في المثال السابق.
بعد سنوات من العمل مع TypeScript في مشاريع مختلفة الأحجام، هذه هي القواعد التي أتبعها لتجنب الأخطاء الشائعة:
TypeScript أداة قوية، لكنها ليست سحرية. هي لا تحميك من الأخطاء المنطقية أو سوء التصميم. لكن إذا استخدمتها بشكل صحيح، يمكنها أن تحول الكود الخاص بك من كود هش يصعب صيانته إلى كود قوي وسهل الفهم. المفتاح هو فهم كيف تعمل TypeScript خلف الكواليس واستخدامها بطريقة تعزز من جودة الكود بدلاً من مجرد إضفاء طبقة تجميلية عليه.
الخطوة التالية؟ افتح مشروعك الحالي وابحث عن هذه الأخطاء الشائعة. ابدأ بتفعيل خيارات TypeScript الصارمة إذا لم تكن مفعلة بالفعل. ثم انتقل إلى الكود وابحث عن استخدام any، union معقدة، وأنواع إرجاع غير محددة. كل تغيير صغير سيجعل مشروعك أكثر قوة واستقراراً على المدى الطويل.