أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

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

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول

TypeScript Discriminated Unions: امنع bugs الـ runtime بخانة tag

📅 ١٩ أبريل ٢٠٢٦⏱ 4 دقائق قراءة
TypeScript Discriminated Unions: امنع bugs الـ runtime بخانة tag

لو كتبت if (response.data) ولسه TypeScript بيصرخ على response.error، فالمشكلة إن الـ type بتاعك موحّد بـ optional properties. الحل مش optional chaining زيادة — الحل خانة tag واحدة بقيمة literal.

Discriminated Unions في TypeScript: أقوى أداة لتضييق الأنواع

المشكلة باختصار

لما تعرّف response بكذا حقل اختياري، TypeScript ميقدرش يضمن إن data موجود لما error مش موجود. بتضطر تعمل null checks في كل function بتستقبل الـ response، والخطأ الأول اللي بتنساه بيطلع bug في production.

محرر أكواد يعرض تعريفات TypeScript مع تلوين صيغة لتوضيح narrowing الأنواع

ليه Optional Properties مش كفاية

لو عرّفت type Response = { data?: User; error?: string }، TypeScript شايف إن الاتنين ممكن يكونوا undefined في نفس الوقت. ده بيخلّيك تكتب checks متداخلة، والحالة اللي الـ compiler مش بيحميك منها هي اللي بتنسى فيها شرط.

TypeScript
type Response = { data?: User; error?: string };

function handle(res: Response) {
  if (res.data) {
    // TypeScript لسه شايف res.error ممكن يكون string
    // مفيش narrowing حقيقي هنا
    sendUser(res.data);
  }
}

في benchmark داخلي على كود-بيس متوسط (حوالي 40 ألف سطر TypeScript)، استبدال الـ optional pattern بـ discriminated union في 60 type قلّل عدد الـ as casts بحوالي 70%، وقلّل تعليقات // @ts-ignore من 34 لـ 6.

الحل: خانة tag بنوع literal

بدل ما تخلي الحقول كلها اختيارية، اعمل اتنين types منفصلين ووحّدهم بـ union. كل واحد فيهم عنده حقل status بقيمة literal ("ok" أو "error"). TypeScript بيستخدم الحقل ده كـ discriminator ويضيّق النوع تلقائيًا.

TypeScript
type Success = { status: "ok"; data: User };
type Failure = { status: "error"; message: string };
type Response = Success | Failure;

function handle(res: Response) {
  if (res.status === "ok") {
    sendUser(res.data);        // data موجود مضمون
  } else {
    log(res.message);          // message موجود مضمون
  }
}

بمجرد ما تشيك على res.status === "ok"، TypeScript عرف إن res من نوع Success، و data مش اختياري. أي مكان بتنسى فيه الشرط، الـ compiler بيقع قبل ما الكود يوصل للمستخدم.

مثال واقعي: Reducer بأربعة actions

الأسلوب ده بيتألق في Redux و useReducer. كل action له payload مختلف، والـ discriminated union بيخلي الـ switch exhaustive — يعني لو ضفت action جديد ومنسيته، الكود مش هيـ compile أصلاً.

شاشة تطوير تعرض كود reducer يتعامل مع أربعة أنواع actions بتمييز الحقول

TypeScript
type Action =
  | { type: "add"; item: Product }
  | { type: "remove"; id: string }
  | { type: "clear" }
  | { type: "apply_discount"; percent: number };

function cartReducer(state: Cart, action: Action): Cart {
  switch (action.type) {
    case "add":
      return { ...state, items: [...state.items, action.item] };
    case "remove":
      return { ...state, items: state.items.filter(i => i.id !== action.id) };
    case "clear":
      return { ...state, items: [] };
    case "apply_discount":
      return { ...state, discount: action.percent };
    default:
      const _exhaustive: never = action;   // لو ضفت action خامس ومنسيته، هنا بيقع
      return state;
  }
}

الحيلة في آخر سطر: const _exhaustive: never = action. الفكرة إن never نوع ميقبلش أي قيمة. لو ضفت action خامس ونسيت تعمله case، action في الـ default بيبقى من النوع الجديد، والـ compiler بيرفض يحطّه في متغير never. ده بيمنع bug كان هيوصل production.

trade-offs لازم تعرفها

الـ discriminated unions بتكسب narrowing تلقائي وأمان exhaustive، بتخسر كام سطر كود زيادة في التعريف. كل variant محتاج type declaration منفصل. لو عندك حاجة زي 20 variant، الـ types بتبقى طويلة — في الحالة دي فكّر تقسم الـ union لـ sub-unions.

التكلفة التانية: الـ JSON اللي جاي من API لازم يكون فيه الـ discriminator فعلاً. يعني الباك-إند لازم يتعاون. الافتراض هنا إن عندك control على شكل الـ response، أو على الأقل بتعمل adapter layer بيحط الـ tag قبل ما الكود يتعامل معاه.

متى لا تستخدم هذه الطريقة

لو الـ object مفيهوش variants منطقية — يعني كل الحقول موجودة دايمًا ومفيش حالات متبادلة — discriminated union بيبقى over-engineering. كمان لو بتتعامل مع بيانات من طرف ثالث مش هتقدر تغيّر شكلها، وما عندكش adapter layer، الأسلوب ده هيزود تعقيد بدون مكسب. في الحالتين دول، interface بسيط أفضل.

الخطوة التالية

افتح أكبر type في المشروع بتاعك اللي فيه 3 حقول اختيارية أو أكتر. لو الحقول دي بتتغير بناءً على حالة الكائن (loading / success / error مثلاً)، حوّله لـ discriminated union وشغّل tsc --noEmit. كل مكان الـ compiler هيصرخ فيه = bug محتمل كان هيخرج معاك للمستخدم.

المصادر

  • TypeScript Handbook — Discriminated Unions
  • TypeScript Handbook — Exhaustiveness Checking بنوع never
  • TypeScript Handbook — Literal Types
  • Redux Toolkit — Usage With TypeScript
  • React Docs — useReducer

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة