تنمذج حالاتك باتحادات مميَّزة وتُثبّت شمولها بنوع never.
افتح هذا الدرس في Kodokonnever هو النوع الفارغ: لا قيمة تسكنه. يظهر حين يُثبت المُصرّف أن مسار كود مستحيل - بعد استنفاد كل أعضاء اتحاد مثلا. هذه الخاصية تجعله أداة تصميم: إن استطعت أن تجعل المُصرّف يقول never في الفرع الافتراضي لـswitch، فلديك برهان استاتيكي أن كل حالة مُعالَجة. هذا هو أساس فحص الشمول (exhaustiveness checking).
اكتشفت الاتحادات المميَّزة في الوحدة السابقة؛ وقد آن أوان تحويلها إلى أداة تصميم حقيقية. مقارنة بواجهة واحدة محشوّة بحقول اختيارية، يجعل الاتحاد المميَّز الحالات المستحيلة غير قابلة للتمثيل: لا يمكن أن تملك data وerror معا، ولا نجاحا بلا بيانات. إنه النمط المفضّل لحالات التحميل ونتائج العمليات ورسائل الأحداث.
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;
}
}صادفت من قبل حارس assertNever المُنادى في الفرع default؛ فلنشرّح الآن كيف يعمل بالضبط. ما دامت كل حالة مغطّاة، يكون لمُعامله هناك نوع never ويُصرَّف كل شيء. أضف صيغة إلى الاتحاد دون تحديث الـswitch، فتصبح الصيغة المنسية غير قابلة للإسناد إلى never: خطأ وقت التصريف، مع اسم الحالة المفقودة في الرسالة. تحوّل بذلك خطأ إنتاج إلى بناء مرفوض.
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);
}
}دقيقتان متقدمتان تستحقان انتباهك. أولا، تضييق الحقل المميِّز ينتشر إلى البنى المتداخلة فقط إذا اختبرت الكائن المُحيط مباشرة؛ أما تفكيك الحقل المميِّز وحده فيكسر الرابط (باستثناء الحالات المُعالَجة منذ TypeScript 4.6 للتفكيك المشترك). ثانيا، never يُمتَص في الاتحادات (string | never هو مجرد string): وهذا ما يتيح للأنواع الشرطية أن ترشّح الصيغ، كما يفعل Exclude.
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));Shape دون تعديل الـswitch الذي يحتوي assertNever؟string | never؟neverstringunknownstring | undefined