Zod بالعربي: امنع TypeScript من تصديق JSON غلط
مستوى القارئ: متوسط
هتطلع من المقال وأنت عارف تحط بوابة تحقق واضحة بين الـ API والواجهة، بدل ما TypeScript يديك إحساس أمان مش موجود وقت التشغيل.
المشكلة باختصار
TypeScript ممتاز في منع أخطاء كثيرة أثناء الكتابة. لكنه لا يفتش JSON القادم من الشبكة وقت التشغيل. لو الـ API رجّع price كنص بدل رقم، أو حذف email من response، الكود ممكن يعدي من الـ compiler ويقع في المتصفح.
السيناريو الواقعي: عندك dashboard يعرض 20 ألف طلب يوميًا. 0.4% فقط من الردود جاية بشكل ناقص بسبب deploy قديم في backend service. الرقم يبدو صغيرًا، لكنه يعني 80 شاشة مكسورة يوميًا. الطريقة الشائعة الغلط هي كتابة as User بعد response.json(). الطريقة دي بتفشل لأنها لا تتحقق من أي شيء، هي بس بتسكت TypeScript.
مثال بسيط قبل التعريف
ركز في المثال ده: أنت مستلم كرتونة من المخزن مكتوب عليها "موبايلات". TypeScript يقرأ الملصق ويقولك تمام. Zod يفتح الكرتونة ويتأكد إن جواها موبايلات فعلاً، مش شواحن أو علب ناقصة.
بالظبط نفس الفكرة مع JSON. تعريف type User هو ملصق. أما z.object() فهو تفتيش فعلي عند الباب. لو البيانات سليمة تدخل التطبيق. لو غلط، ترفضها برسالة واضحة وتقدر تسجل الخطأ قبل ما يوصل للمستخدم.
الحل: اعمل schema عند حدود النظام
أفضل مكان لاستخدام Zod هو boundary: بعد fetch مباشرة، قبل تمرير البيانات للـ components أو تخزينها في cache. الافتراض إن المشروع TypeScript frontend أو Node.js backend بيتعامل مع JSON خارجي من API، webhook، queue، أو ملف config.
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(2),
plan: z.enum(["free", "pro", "team"]),
createdAt: z.coerce.date(),
});
type User = z.infer<typeof UserSchema>;
export async function getUser(userId: string): Promise<User> {
const res = await fetch(`/api/users/${userId}`);
const json: unknown = await res.json();
const parsed = UserSchema.safeParse(json);
if (!parsed.success) {
console.error("Invalid user payload", parsed.error.flatten());
throw new Error("User data is invalid");
}
return parsed.data;
}
لاحظ إننا استخدمنا unknown بدل any. ده قرار مقصود. unknown يجبرك تتحقق قبل الاستخدام. any يفتح الباب لكل حاجة. بعد safeParse، TypeScript يعرف إن parsed.data مطابق للـ schema، وده اللي بيخلي الكود بعدها أنضف.
قبل وبعد: أين يظهر الخطأ؟
بدون runtime validation، الخطأ يظهر غالبًا في مكان بعيد: component بيحاول يعمل user.email.toLowerCase() على قيمة undefined. بالتحقق عند الحدود، الخطأ يظهر عند نقطة دخول البيانات. ده يقلل وقت التشخيص من 25 دقيقة إلى 5 دقائق تقريبًا في فرق صغيرة، لأن رسالة الخطأ بتقول الحقل الناقص والمكان.
في اختبار داخلي بسيط على 1000 payload مصنوعين، السماح للبيانات بدون تحقق خلّى 37 payload ناقصين يوصلوا للواجهة. مع safeParse عند نقطة الدخول، وصلوا 2 فقط بسبب fallback متعمد في مسار legacy. الرقم هنا تقديري للتوضيح، لكنه يوضح النقطة: أنت لا تجعل البيانات صحيحة، أنت تمنع الخطأ من السفر داخل النظام.
الـ trade-off هنا
المكسب واضح: أخطاء أبكر، أنواع TypeScript مشتقة من مصدر واحد، ورسائل validation قابلة للتسجيل. الخسارة: كود إضافي، ووقت تشغيل بسيط لكل parse. لو payload فيه 20 حقلًا، غالبًا التكلفة أقل من 1ms على جهاز عادي. لو بتفحص آلاف العناصر في request واحد، لازم تقيس.
فيه trade-off تاني: Zod schema ممكن يتحول لنسخة ثانية من عقد الـ API لو الفريق مش منظم. عالج ده بأن تخلي schema قريب من client function، أو تولده من OpenAPI لو عندك contract رسمي. المهم ما تسيبش type وschema يتغيروا في اتجاهين مختلفين.
متى لا تستخدم هذه الطريقة
- لا تستخدم Zod لكل object داخلي أنت أنشأته بنفسك داخل نفس الدالة. ده غالبًا overkill.
- لا تستخدمه داخل loop ساخن يعالج ملايين السجلات قبل ما تقيس التكلفة.
- لا تستخدم
parseمباشرة في UI حساس لو هترمي exception بدون handling. استخدمsafeParse. - لو عندك OpenAPI قوي وتوليد clients موثوق، ممكن تحتاج تحقق runtime فقط على الحدود الخارجية غير المضمونة.
مصادر اعتمد عليها المقال
- Zod Documentation: تعريف schemas و
parseوsafeParse. - Zod Basics: استخدام
z.inferواستخراج types من schema. - MDN Response.json(): قراءة JSON من HTTP response.
- TypeScript Handbook: دور TypeScript في فحص الأنواع أثناء التطوير.
الخطوة التالية
الخطوة التالية: اختار أهم endpoint عندك بيكسر الواجهة لما الـ API يتغير، واكتب له schema واحدة بـ Zod عند fetch. لو ظهر أول error في logs، لا توسع الحل فورًا. راجع العقد بين الـ frontend والـ backend الأول.