لو كتبت 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.
ليه Optional Properties مش كفاية
لو عرّفت type Response = { data?: User; error?: string }، TypeScript شايف إن الاتنين ممكن يكونوا undefined في نفس الوقت. ده بيخلّيك تكتب checks متداخلة، والحالة اللي الـ compiler مش بيحميك منها هي اللي بتنسى فيها شرط.
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 ويضيّق النوع تلقائيًا.
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 بيقع قبل ما الكود يوصل للمستخدم.