تخطي إلى المحتوى الرئيسي

أنواع الاتحاد (Union Types) وأنواع التقاطع (Intersection Types) في TypeScript

أنواع الاتحاد (Union Types) وأنواع التقاطع (Intersection Types) في TypeScript

الدليل الشامل لأنواع الاتحاد (Union Types) والتقاطع (Intersection Types) في TypeScript: من الأساسيات النظرية إلى الأنماط المعمارية المتقدمة

القسم 1: المقدمة والنظرة الشاملة

في هذه الدورة التدريبية “دورة TypeScript من الصفر إلى الاحتراف باللغة العربية” (الدرس #16)، سنتعرف على مفهوم أنواع الاتحاد والتقاطع في لغة TypeScript. تعتبر هذه المفاهيم حجر الزاوية في بناء نماذج بيانات دقيقة، مرنة، وخالية من الثغرات، كما تُشكل جوهر نظام الأنواع الهيكلي (Structural Type System) الذي يميز TypeScript عن لغات البرمجة الثابتة التقليدية مثل Java وC#.

1.1. ما هي أنواع الاتحاد (Union Types)؟

أنواع الاتحاد (Union Types) هي وسيلة لنمذجة القيم التي يمكن أن تتخذ شكلاً واحداً من بين عدة خيارات محددة مسبقاً في لغة TypeScript. نستخدم الرمز | للتعبير عن أن المتغير يمكن أن ينتمي إلى النوع الأول أو الثاني أو خيارات أخرى مضافة:

// تعريف متغير يقبل إما نصاً أو رقماً
let identifier: string | number;

identifier = "USR-9801"; // ✅ مسموح
identifier = 10492;      // ✅ مسموح
// identifier = true;    // ❌ خطأ ترجمة فوري: Type 'boolean' is not assignable to type 'string | number'

1.2. ما هي أنواع التقاطع (Intersection Types)؟

أنواع التقاطع (Intersection Types) هي وسيلة لدمج تعريفات وخصائص عدة أنواع معاً لتكوين نوع جديد يمتلك كافة الحقول والوظائف مجتمعة. يُستخدم الرمز & للتعبير عن أن الكائن يجب أن يستوفي متطلبات النوع الأول والنوع الثاني بالكامل دون نقصان:

interface Identifiable {
    id: string;
}

interface Loggable {
    log(): void;
}

// دمج الواجهتين لتكوين نوع جديد يشترط وجود الخاصيتين معاً
type Entity = Identifiable & Loggable;

const item: Entity = {
    id: "ENT-101",
    log() {
        console.log(`سجل الكيان: ${this.id}`);
    }
};

1.3. حالات الاستخدام العملية لأنواع الاتحاد والتقاطع

  • تحديد القيم المقبولة لأنواع البيانات: قصر المدخلات على نصوص حرفية محددة مثل الحالات ("pending" | "resolved" | "rejected") بدلاً من قبول أي نص عام.
  • تعزيز الأمان النوعي (Type Safety): منع تمرير معاملات غير متوقعة تؤدي إلى استثناءات وانهيارات في وقت التشغيل.
  • توحيد الكود وتوثيقه الذاتي: جعل توقيعات الدوال واضحة ومقروءة دون الحاجة لتعليقات توضيحية مطولة.
  • إعادة استخدام الخصائص وتركيب الكائنات: دمج نماذج قواعد البيانات مع سمات التدقيق (Audit Properties) وسجلات التتبع بسهولة عبر التقاطع.
أنواع الاتحاد (Union Types – |)أنواع التقاطع (Intersection Types – &)

استخدامات أنواع الاتحاد:

  • تحديد القيم المقبولة لمجموعات المتغيرات المتنوعة.
  • معالجة استجابات الـ APIs التي قد تعود ببيانات أو أخطاء.
  • التحقق الشرطي والتضييق عبر حراس الأنواع (Type Guards).

تسمح بحصر الحالات الممكنة، مثل تمثيل حالة الطلب التجاري أو قبول معرّف رقمي أو نصي.

استخدامات أنواع التقاطع:

  • دمج خصائص عدة كائنات في نموذج بيانات موحد.
  • تطبيق نمط التركيب على الوراثة (Composition over Inheritance).
  • إضافة بيانات الميتا والحقول الوظيفية للنماذج الأصلية.

تفرض استيفاء كل خصائص الأنواع المتقاطعة، مثل الجمع بين كائن المستخدم وبيانات الصلاحيات الأمنية.

القسم 2: المفاهيم الأساسية والأهمية النظرية

في نظرية المجموعات الرياضية (Set Theory)، ينظر محرك TypeScript إلى كل “نوع” باعتباره مجموعة من القيم المحتملة في الذاكرة. يمثل النوع string مجموعة كل السلاسل النصية الممكنة، بينما يمثل النوع number مجموعة كافة الأرقام الحقيقية والصحيحة:

  • الاتحاد (Union – $A \cup B$): هو ضم جميع القيم في المجموعة $A$ إلى المجموعة $B$. وبالتالي، فإن المتغير من نوع A | B يمكنه أن يحتوي على أي قيمة تنتمي لأي من المجموعتين. لكن من منظور الوصول إلى الخصائص على الكائنات المشتركة، يسمح المترجم فقط بالوصول إلى الخصائص المشتركة الموجودة في كلا النوعين ما لم يتم التضييق.
  • التقاطع (Intersection – $A \cap B$): هو إيجاد المنطقة المشتركة للقيم التي تنتمي إلى المجموعة $A$ والمجموعة $B$ في آن واحد. فإذا قاطعت نوعين بدائيين غير متوافقين مثل string & number، ستكون النتيجة حتماً never، لأنه يستحيل وجود قيمة عددية ونصية في نفس اللحظة. أما عند تقاطع كائنات، فالتقاطع يعني جمع كافة الحقول معاً.
العملية المنطقيةالصياغة في TypeScriptالنتيجة المنطقية على الكائناتالنتيجة على الأنواع البدائية المتناقضة
الاتحاد (OR المنطقي)TypeA | TypeBالخصائص المشتركة بينهما فقط (قبل التضييق)قبول أي من القيمتين البدائيتين
التقاطع (AND المنطقي)TypeA & TypeBمجموع كافة الخصائص في TypeA و TypeB معاًتتحول القيمة تلقائياً إلى never

القسم 3: الشرح التفصيلي والخطوات العملية

ننتقل الآن للخطوات التنفيذية لصياغة واستخدام أنواع الاتحاد والتقاطع برمجياً في سيناريوهات الكود الواقعية:

3.1. تطبيق أنواع الاتحاد الأساسية والحرفية (Literal Unions)

يمكن دمج الأنواع الأساسية، أو تحديد مجموعة قيم نصية أو رقمية ثابتة عبر ما يعرف بـ Literal Types:

// اتحاد نصي حرفي يحدد الاتجاهات المسموحة فقط
type Direction = "North" | "South" | "East" | "West";

function move(direction: Direction, distanceMeters: number): void {
    console.log(`التحرك نحو ${direction} لمسافة ${distanceMeters} متراً.`);
}

move("North", 150); // ✅ مسموح
// move("Upward", 20); // ❌ خطأ فوري: غير موجود ضمن قيم الاتحاد

3.2. تطبيق أنواع التقاطع وتوسيع الكائنات

يُعد التقاطع بديلاً مرناً وقوياً للوراثة بالفئات (Class Inheritance) والواجهات (Interface Extends) عند الحاجة إلى جمع بنيات بيانات متباينة:

interface Timestamped {
    createdAt: Date;
    updatedAt: Date;
}

interface SoftDeletable {
    isDeleted: boolean;
    deletedAt?: Date;
}

interface UserRecord {
    id: number;
    username: string;
    email: string;
}

// إنشاء نوع يجمع سجل المستخدم مع بيانات التتبع الزمني والحذف الناعم
type FullDatabaseUser = UserRecord & Timestamped & SoftDeletable;

const databaseUser: FullDatabaseUser = {
    id: 101,
    username: "coder_mohammad",
    email: "mohammad@code-elta6ur.com",
    createdAt: new Date("2026-01-10"),
    updatedAt: new Date(),
    isDeleted: false
};

القسم 4: تقنيات تضييق الأنواع (Type Narrowing) وحراس الأنواع (Type Guards)

عند التعامل مع نوع اتحاد string | number، لا يمكنك استدعاء دوال خاصة بالسلاسل مثل .toUpperCase() أو دوال عددية مثل .toFixed() مباشرة، لأن TypeScript لا يضمن لك نوع القيمة الفعلي في تلك اللحظة. هنا يأتي دور تضييق الأنواع (Type Narrowing) عبر الحراس البرمجية:

1. التضييق بواسطة typeof

function formatSalary(salary: number | string): string {
    if (typeof salary === "number") {
        // TypeScript يعلم يقيناً هنا أن salary هو number
        return `$${salary.toFixed(2)}`;
    } else {
        // TypeScript يستنتج تلقائياً أن salary هنا هو string
        return `$${parseFloat(salary).toFixed(2)}`;
    }
}

2. التضييق بواسطة instanceof

class FileLogger {
    logToFile(msg: string) { console.log(`ملف: ${msg}`); }
}

class ConsoleLogger {
    logToConsole(msg: string) { console.log(`شاشة: ${msg}`); }
}

function executeLog(logger: FileLogger | ConsoleLogger, message: string) {
    if (logger instanceof FileLogger) {
        logger.logToFile(message);
    } else {
        logger.logToConsole(message);
    }
}

3. التضييق بواسطة المعامل in للتحقق من وجود الخصائص

interface AdminUser {
    id: string;
    permissions: string[];
}

interface StandardUser {
    id: string;
    profileUrl: string;
}

function processUser(account: AdminUser | StandardUser) {
    if ("permissions" in account) {
        console.log(`صلاحيات المدير: ${account.permissions.join(", ")}`);
    } else {
        console.log(`رابط الملف الشخصي: ${account.profileUrl}`);
    }
}

4. أنواع الاتحاد المميزة (Discriminated Unions) وفحص الشمولية (Exhaustiveness Checking)

تُعد الاتحادات المميزة (Discriminated Unions) النمط المعماري الأقوى في لغة TypeScript لإدارة الحالات المعقدة؛ وتعتمد على تزويد كل نوع في الاتحاد بحقل حرفي مشترك (Discriminant Literal Key) مثل status أو kind أو type:

interface LoadingState {
    status: "loading";
}

interface SuccessState {
    status: "success";
    data: string[];
}

interface ErrorState {
    status: "error";
    error: Error;
}

type NetworkState = LoadingState | SuccessState | ErrorState;

function renderNetworkResponse(state: NetworkState): string {
    switch (state.status) {
        case "loading":
            return "جاري تحميل البيانات...";
        case "success":
            return `تم استلام ${state.data.length} عناصر بنجاح.`;
        case "error":
            return `حدث خطأ غير متوقع: ${state.error.message}`;
        default:
            // فحص الشمولية الصارم (Exhaustiveness Check):
            // إذا تمت إضافة حالة جديدة مستقبلاً ولم تُعالج هنا، سيرمي الكود خطأ ترجمة فوري!
            const _exhaustiveCheck: never = state;
            return _exhaustiveCheck;
    }
}

القسم 5: أفضل الممارسات والنصائح التطبيقية

لضمان بناء بنى تحتية برمجية صلبة وسهلة الفهم والصيانة، اتبع الممارسات المعمارية التالية:

  • تجنب التقاطعات المستحيلة (Avoid Impossible Intersections): لا تقم بمقاطعة أنواع بدائية متناقضة أو كائنات تشترك في حقل يحمل أنواعاً بدائية متضاربة دون تمييز، لأن النتيجة ستكون النوع الميت never.
  • اعتمد دوماً على Discriminated Unions لإدارة تدفق البيانات: بدلاً من تخمين الحقول عبر in، حدد حقلاً صريحاً مثل kind يوضح طبيعة الكائن فوراً في بنية الـ Redux أو إدارة الحالة.
  • استخدم فحص الشمولية الصارم بواسطة النوع never: في جمل switch أو سلاسل if/else، اجعل فرع default يحاول إسناد القيمة إلى never لاكتشاف الحالات غير المغطاة أثناء التطوير.
  • استخرج أسماء الأنواع المستعارة (Type Aliases) للاتحادات المتكررة: بدلاً من تكرار string | number | undefined في كل توقيع دالة، عرف نوعاً مستعاراً نظيفاً لتسهيل التعديل المستقبلي.
  • افصل نماذج طلب البيانات عن نماذج التخزين: استخدم التقاطع لدمج الحقول العامة (مثل معرف الكيان وتاريخ الإضافة) مع كائنات الإدخال لإنتاج سجلات النظام دون تكرار الحقول يدوياً.

القسم 6: حل المشكلات والأخطاء الشائعة

نستعرض فيما يلي أكثر المشاكل البرمجية التي تحير المطورين عند العمل مع Union و Intersection وكيفية التعامل معها وحلها:

المشكلة 1: Property does not exist on type ‘UnionType’

السبب: محاولة قراءة خاصية توجد في أحد أعضاء الاتحاد فقط دون التحقق منها مسبقاً. يفترض TypeScript أن المتغير يحتوي فقط على التقاطع المشترك بين الحقول.

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function operatePet(pet: Fish | Bird) {
    // ❌ خطأ: Property 'swim' does not exist on type 'Fish | Bird'
    // pet.swim(); 

    // ✅ الحل: التضييق عبر فحص الملكية
    if ("swim" in pet) {
        pet.swim();
    } else {
        pet.fly();
    }
}

المشكلة 2: تحول نوع التقاطع إلى never نتيجة تضارب الخصائص

السبب: عند مقاطعة نوعين يمتلكان نفس اسم الحقل ولكن بأنواع بيانات بدائية غير متوافقة (مثلاً: أحدهما يطلب id: string والآخر id: number)، يتحول الحقل إلى string & number أي never، مما يجعل إنشاء الكائن مستحيلاً عملياً:

type LegacySystem = { id: string; name: string };
type NewSystem = { id: number; metadata: object };

// ⚠️ خطر: id هنا أصبح من نوع never!
type Combined = LegacySystem & NewSystem;

// ❌ خطأ ترجمة مستحيل الإرضاء:
// const record: Combined = { id: 101, name: "تجربة", metadata: {} };
// Type 'number' is not assignable to type 'never'.

// ✅ الحل: استخدام Omit لحذف الحقل المتضارب ثم إعادة تعريفه بمرونة
type SafeCombined = Omit<LegacySystem, "id"> & Omit<NewSystem, "id"> & { id: string | number };

const safeRecord: SafeCombined = {
    id: "REC-2026",
    name: "سجل متوافق",
    metadata: { valid: true }
};

المشكلة 3: التوزيع التلقائي في الأنواع الشرطية (Distributive Conditional Types)

السبب: عند تمرير نوع اتحاد لمعامل نوع عام (Generic Parameter) يحتوي على شرط، يوزع TypeScript الشرط على كل عنصر في الاتحاد بشكل مستقل، مما قد يغير شكل النتيجة غير المقصود. لحل ذلك، يتم تغليف النوع بين أقواس معقوفة [T] extends [any] لتعطيل التوزيع عند الحاجة.

القسم 7: دراسات حالة وأمثلة واقعية متكاملة

دراسة الحالة 1: محرك معالجة الملفات والوثائق المتعددة (Document Processing Engine)

في تطبيقات إدارة المحتوى والأنظمة السحابية، نحتاج لمعالجة أنواع مختلفة من الملفات (صور، مستندات نصية، مقاطع صوتية) مع خصائص ومحددات مستقلة لكل نوع:

interface ImageDocument {
    mimeType: "image/png" | "image/jpeg";
    width: number;
    height: number;
    compressionQuality: number;
}

interface TextDocument {
    mimeType: "application/pdf" | "text/plain";
    pageCount: number;
    wordCount: number;
}

interface AudioDocument {
    mimeType: "audio/mp3" | "audio/wav";
    durationSeconds: number;
    bitrate: number;
}

// استخدام اتحاد مميز لتصنيف الوثيقة
type UploadedFile = (ImageDocument | TextDocument | AudioDocument) & {
    fileId: string;
    fileName: string;
    sizeBytes: number;
};

function inspectUploadedFile(file: UploadedFile): void {
    console.log(`معالجة الملف: ${file.fileName} بحجم ${file.sizeBytes} بايت.`);

    switch (file.mimeType) {
        case "image/png":
        case "image/jpeg":
            console.log(`أبعاد الصورة: ${file.width}x${file.height} بجودة ضغط ${file.compressionQuality}%`);
            break;
        case "application/pdf":
        case "text/plain":
            console.log(`المستند النصي يتضمن ${file.pageCount} صفحة و ${file.wordCount} كلمة.`);
            break;
        case "audio/mp3":
        case "audio/wav":
            console.log(`الملف الصوتي مدته ${file.durationSeconds} ثانية ومعدل البت ${file.bitrate} kbps.`);
            break;
    }
}

const userAvatar: UploadedFile = {
    fileId: "FILE-889",
    fileName: "profile.png",
    sizeBytes: 204800,
    mimeType: "image/png",
    width: 500,
    height: 500,
    compressionQuality: 90
};

inspectUploadedFile(userAvatar);

دراسة الحالة 2: نمذجة استجابات واجهات برمجة التطبيقات (API Response Wrapper)

تصميم نمط موحد لمعالجة استجابات الخوادم، يفصل تماماً بين حالة النجاح ببياناتها وحالة الفشل بأكواد أخطائها ورسائلها:

interface ApiSuccess<T> {
    status: 200 | 201;
    isSuccess: true;
    payload: T;
    timestamp: number;
}

interface ApiFailure {
    status: 400 | 401 | 403 | 404 | 500;
    isSuccess: false;
    error: {
        code: string;
        message: string;
    };
    timestamp: number;
}

type ApiResponse<T> = ApiSuccess<T> | ApiFailure;

interface ProductData {
    productId: number;
    title: string;
    price: number;
}

function handleServerResponse(res: ApiResponse<ProductData>) {
    if (res.isSuccess) {
        // TypeScript يتيح فوراً قراءة payload بنوع ProductData بأمان
        console.log(`تم جلب المنتج: ${res.payload.title} بقيمة $${res.payload.price}`);
    } else {
        // استنتاج مؤكد لوجود حقل error وكود الفشل
        console.error(`خطأ ${res.status}: [${res.error.code}] ${res.error.message}`);
    }
}

القسم 8: الأسئلة الشائعة وإجاباتها التفصيلية

1. ما الفرق الجوهري بين الواجهات (Interfaces) وأنواع التقاطع (Intersection Types)؟

الواجهات تُستخدم لتحديد هياكل الكائنات ويمكن توسيعها عبر extends، وتدعم ميزة الدمج التلقائي للتصريحات (Declaration Merging). أما أنواع التقاطع & فهي أداة تعبيرية أوسع نطاقاً؛ حيث تستطيع دمج أي أنواع (أنواع كائنات، أنواع حرفية، ودوال) دون تقيد بقواعد الواجهات التقليدية، ولا تقبل دمج التصريحات التلقائي.

2. لماذا لا يُسمح لي بقراءة خاصية توجد في أحد طرفي نوع الاتحاد دون فحص مسبق؟

لأن TypeScript صُممت لتضمن سلامة وقت التشغيل (Runtime Safety). إذا سمح لك المترجم بقراءة خاصية قد لا تكون موجودة إذا أخذ المتغير الشكل الآخر للاتحاد، فسينهار البرنامج عند التشغيل بخطأ TypeError: Cannot read properties of undefined. لذا يلزمك المترجم بتضييق النوع مسبقاً.

3. كيف أتعامل مع مصفوفات تحتوي على أنواع متعددة مختلطة؟

يتم ذلك بوضع نوع الاتحاد بين قوسين قبل علامة المصفوفة: (string | number)[]. تنبيه: كتابة string | number[] تعني أن المتغير إما نص فردي أو مصفوفة من الأرقام، لذلك فإن الأقواس ضرورية جداً لتحديد اتحاد عناصر المصفوفة.

4. ماذا يحدث عند مقاطعة نوع يحتوي على حقل اختياري مع نوع يطلب نفس الحقل كإلزامي؟

في التقاطع { name?: string } & { name: string }، تكون النتيجة هي الإلزامية { name: string }، لأن التقاطع يشترط استيفاء القيد الأقوى الأكثر دقة لضمان توافق الكائن مع كلا الطرفين في آن واحد.

الخلاصة والخطوات القادمة

في ختام هذا الدرس المفصل من دورة TypeScript من الصفر إلى الاحتراف (الدرس #16)، نلخص أهم المكتسبات المعمارية التي استعرضناها:

  • أنواع الاتحاد (Union Types – |): تجسد مفهوم الخيارات المتعددة المنطقية (Logical OR) وتمكنك من نمذجة الحالات المتنوعة وتضييقها بحراس الأنواع بدقة تامة.
  • أنواع التقاطع (Intersection Types – &): تجسد مفهوم الجمع والتركيب المنطقي (Logical AND) وتسمح بدمج واجهات وخصائص ونماذج بيانات متعددة في كيان موحد وشامل.
  • الاتحادات المميزة (Discriminated Unions): تمثل الأسلوب الأفضل لتصميم استجابات الشبكة، وحالات واجهات المستخدم، ومحركات الأحداث بدون غموض أو أخطاء خفية.
  • فحص الشمولية الصارم (Exhaustiveness Checking): يضمن مواكبة كودك لأي توسعات وتحديثات مستقبلية في نماذج البيانات عبر حراسة النوع never.

في الدروس القادمة، سنبني فوق هذه المفاهيم للتعرف على الأنواع المخططة (Mapped Types) والأنواع الشرطية المتقدمة (Conditional Types) وكيفية التلاعب بمفاتيح الكائنات عبر keyof و typeof للوصول إلى أعلى مستويات الاحتراف البرمجي في TypeScript.

المراجع والمصادر الرسمية المعتمدة:


اكتشاف المزيد من كود التطور

اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.

🎓 التقدم التعليمي في هذا الدرس
هل أتممت فهم وتطبيق هذا الدرس؟ اضغط على الزر لتسجيل إنجازه وكسب 10 نقاط!
MOHAMMAD ABU KASHREEF

MOHAMMAD ABU KASHREEF

محمد، مهندس برمجيات وصانع محتوى تقني، مؤسس كود التطور. أشارك خبرتي في البرمجة والتقنية بأسلوب عملي وتجارب واقعية، بهدف تبسيط المعرفة وبناء محتوى عربي موثوق وقابل للتطبيق.

اترك رد