Kodokon kodokon.com

never والشمول والاتحادات المميَّزة المتقدمة

تنمذج حالاتك باتحادات مميَّزة وتُثبّت شمولها بنوع never.

10 دقيقة · 3 أسئلة

افتح هذا الدرس في Kodokon

never هو النوع الفارغ: لا قيمة تسكنه. يظهر حين يُثبت المُصرّف أن مسار كود مستحيل - بعد استنفاد كل أعضاء اتحاد مثلا. هذه الخاصية تجعله أداة تصميم: إن استطعت أن تجعل المُصرّف يقول never في الفرع الافتراضي لـswitch، فلديك برهان استاتيكي أن كل حالة مُعالَجة. هذا هو أساس فحص الشمول (exhaustiveness checking).

اكتشفت الاتحادات المميَّزة في الوحدة السابقة؛ وقد آن أوان تحويلها إلى أداة تصميم حقيقية. مقارنة بواجهة واحدة محشوّة بحقول اختيارية، يجعل الاتحاد المميَّز الحالات المستحيلة غير قابلة للتمثيل: لا يمكن أن تملك data وerror معا، ولا نجاحا بلا بيانات. إنه النمط المفضّل لحالات التحميل ونتائج العمليات ورسائل الأحداث.

TYPESCRIPT
type FetchState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error };

function render(state: FetchState<string[]>): string {
  switch (state.status) {
    case "idle": return "Waiting";
    case "loading": return "Loading…";
    case "success": return state.data.join(", ");
    case "error": return state.error.message;
  }
}
data لا يوجد إلا في صيغة success: لا وصول إليه في أي مكان آخر.

صادفت من قبل حارس assertNever المُنادى في الفرع default؛ فلنشرّح الآن كيف يعمل بالضبط. ما دامت كل حالة مغطّاة، يكون لمُعامله هناك نوع never ويُصرَّف كل شيء. أضف صيغة إلى الاتحاد دون تحديث الـswitch، فتصبح الصيغة المنسية غير قابلة للإسناد إلى never: خطأ وقت التصريف، مع اسم الحالة المفقودة في الرسالة. تحوّل بذلك خطأ إنتاج إلى بناء مرفوض.

TYPESCRIPT
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number }
  | { kind: "rect"; width: number; height: number };

function assertNever(value: never): never {
  throw new Error("Unhandled case: " + String(value));
}

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle": return Math.PI * shape.radius ** 2;
    case "square": return shape.side ** 2;
    case "rect": return shape.width * shape.height;
    default: return assertNever(shape);
  }
}
إضافة صيغة إلى Shape تكسر تصريف area.

دقيقتان متقدمتان تستحقان انتباهك. أولا، تضييق الحقل المميِّز ينتشر إلى البنى المتداخلة فقط إذا اختبرت الكائن المُحيط مباشرة؛ أما تفكيك الحقل المميِّز وحده فيكسر الرابط (باستثناء الحالات المُعالَجة منذ TypeScript 4.6 للتفكيك المشترك). ثانيا، never يُمتَص في الاتحادات (string | never هو مجرد string): وهذا ما يتيح للأنواع الشرطية أن ترشّح الصيغ، كما يفعل Exclude.

TYPESCRIPT
type Event =
  | { type: "click"; x: number; y: number }
  | { type: "keypress"; key: string };

type EventOf<T extends Event["type"]> =
  Extract<Event, { type: T }>;

type ClickEvent = EventOf<"click">;

function handle<T extends Event["type"]>(
  type: T,
  handler: (event: EventOf<T>) => void
): void {
  /* register the handler */
}

handle("click", (e) => console.log(e.x, e.y));
Extract يرشّح الاتحاد بالحقل المميِّز: المعالِج دقيق.

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. لماذا يُفضَّل الاتحاد المميَّز على واجهة بحقول اختيارية لحالة تحميل؟
    • يستهلك ذاكرة أقل وقت التشغيل
    • يجعل التركيبات المتناقضة (data وerror في آن واحد) غير قابلة للتمثيل
    • يتفادى كتابة عبارات switch
    • إنه إلزامي في الوضع الصارم
  2. ماذا يحدث إن أضفت صيغة إلى Shape دون تعديل الـswitch الذي يحتوي assertNever؟
    • يُرمى استثناء عند كل نداء لـarea
    • يُصرَّف الكود لكن الصيغة الجديدة تُعيد undefined
    • يفشل التصريف لأن الصيغة المنسية غير قابلة للإسناد إلى never
  3. ما هو النوع string | never؟
    • never
    • string
    • unknown
    • string | undefined