🛡️ احتراف الـ Type Guards في TypeScript: كيف تتحكم في أنواع بياناتك؟

في الدروس السابقة، تعلمنا كيف نحدد أنواع البيانات (Types). ولكن ماذا يحدث عندما يكون لدينا متغير يمكن أن يحمل "أكثر من نوع" في نفس الوقت؟ هنا تظهر مشكلة: كيف نخبر TypeScript بأن هذا المتغير الآن هو من نوع معين حتى نتمكن من استخدام الخصائص الخاصة بهذا النوع دون أن يظهر لنا خطأ؟

هنا يأتي دور Type Guards (حراس الأنواع). ببساطة، هي طريقة لإخبار TypeScript: "تأكد أولاً أن هذا المتغير هو من نوع (كذا)، وإذا كان كذلك، اسمح لي باستخدام ميزاته".


🧐 ما هي مشكلة "تعدد الأنواع" (Union Types)؟

قبل أن نشرح الحل، دعنا نرى المشكلة. تخيل أن لديك دالة تستقبل إما نصاً (string) أو رقماً (number).

function printLength(value: string | number) {
    // هنا سيقوم TypeScript بإعطائك خطأ! ❌
    // لأن خاصية length موجودة في النصوص فقط وليست في الأرقام
    console.log(value.length); 
}

في المثال أعلاه، TypeScript يرفض تشغيل الكود لأنه لا يضمن أن value ستكون دائماً نصاً. ماذا لو كانت رقماً؟ الأرقام ليس لها length! 😱

الحل هو استخدام Type Guards لعمل "تضييق" (Narrowing) للنوع.


1️⃣ استخدام typeof (لأنواع البيانات الأساسية) 🔍

تعتبر typeof هي الطريقة الأبسط والأكثر استخداماً للتحقق من الأنواع الأساسية مثل (string, number, boolean).

لنقم بتعديل المثال السابق:

function printLength(value: string | number) {
    if (typeof value === "string") {
        // داخل هذا القوس، TypeScript متأكد 100% أن value هي string
        console.log("The length is: " + value.length); // ✅ تعمل الآن بنجاح
    } else {
        // هنا TypeScript يستنتج تلقائياً أن value يجب أن تكون number
        console.log("This is a number, it has no length!"); // ✅
    }
}

printLength("Hello"); // Output: The length is: 5
printLength(100);     // Output: This is a number, it has no length!

ماذا حدث هنا؟ 💡 بمجرد كتابة if (typeof value === "string") قام TypeScript بعملية تسمى Type Narrowing. أي أنه "ضيّق" الاحتمالات، فداخل الـ if أصبح المتغير نصاً فقط.


2️⃣ استخدام instanceof (للكائنات والصفوف) 🏗️

عندما نتعامل مع كائنات (Objects) أو صفوف (Classes)، لا يمكننا استخدام typeof لأنها ستخبرنا ببساطة أن النوع هو "object". هنا نستخدم instanceof.

مثال بسيط: لنفترض أن لدينا صنفاً (Class) يمثل "مستخدم".

class User {
    name: string = "Guest";
}

class Admin {
    role: string = "Super Admin";
}

function checkUserRole(person: User | Admin) {
    if (person instanceof Admin) {
        // هنا TypeScript يعرف أن الشخص هو Admin
        console.log("Welcome Admin! Your role is: " + person.role); // ✅
    } else {
        // هنا TypeScript يعرف أنه User عادي
        console.log("Welcome User! Your name is: " + person.name); // ✅
    }
}

const user1 = new User();
const admin1 = new Admin();

checkUserRole(user1);  // Output: Welcome User! Your name is: Guest
checkUserRole(admin1); // Output: Welcome Admin! Your role is: Super Admin

3️⃣ التحقق من وجود خاصية معينة (In Operator) 🔑

أحياناً لا نملك class أو نوعاً أساسياً، بل نملك "كائنات" مختلفة ونريد التأكد من وجود خاصية معينة بداخلها باستخدام الكلمة المحجوزة in.

مثال توضيحي:

interface Fish {
    swim: () => void;
}

interface Bird {
    fly: () => void;
}

function move(animal: Fish | Bird) {
    if ("swim" in animal) {
        // إذا كانت خاصية swim موجودة، إذن هذا Fish
        animal.swim(); // ✅
    } else {
        // خلاف ذلك، هو Bird
        animal.fly(); // ✅
    }
}

const myFish = { swim: () => console.log("Swimming...") };
const myBird = { fly: () => console.log("Flying...") };

move(myFish); // Output: Swimming...
move(myBird); // Output: Flying...

📝 ملخص سريع للـ Type Guards

الأداة متى نستخدمها؟ مثال
typeof مع الأنواع الأساسية (string, number, etc) typeof val === 'string'
instanceof مع الصفوف (Classes) والكائنات المنشأة منها val instanceof User
in للتأكد من وجود خاصية (Property) داخل كائن 'name' in val