من JSON إلى TypeScript: واجهات interface وحرّاس الأنواع وحدود التوليد
الخلاصة: توليد interface من عينة JSON نصف المهمة: الحقول الاختيارية وnull والاتحادات والتواريخ والأعداد الكبيرة تحتاج مراجعة بشرية، والأنواع لا تقدم تحققًا في وقت التشغيل.

الخلاصة: ولّد interface لتوفير الكتابة، ثم أعد فحص الاختيارية وnull، وأبقِ التواريخ والأعداد الكبيرة نصوصًا، وأضف تحققًا في وقت التشغيل على الحدود؛ فمعاملة الأنواع المولّدة كعقد تعني عدم وجود تحقق على الإطلاق.
لا يرى المولّد إلا العينة الواحدة التي تلصقها فيه. وهل يمكن أن تغيب الحقول، وهل يمكن أن تكون القيم null، وأي صيغة تستخدم التواريخ — كلها تخمينات، والتخمينات الخاطئة عليك إصلاحها.
1. ما الذي يفعله المولّد فعلًا
الصق عينة JSON في من JSON إلى TypeScript فالـ interface الذي تحصل عليه هو ملخص للشكل: يعد المفاتيح وأنواع القيم التي رآها، ثم يختار أضيق تمثيل متاح.
{
"id": 1,
"email": "ada@example.com",
"profile": { "nickname": "Ada", "age": 36 },
"tags": ["admin", "beta"],
"createdAt": "2026-10-31T08:00:00Z"
}
ويبدو الناتج تقريبًا هكذا:
interface User {
id: number;
email: string;
profile: { nickname: string; age: number };
tags: string[];
createdAt: string;
}
هذا يُصرَّف (compiles)، لكنه يصف قيمة مثالية لا العقد الحقيقي لنقطة النهاية. فلا يعرف المولّد هل يمكن أن يغيب email، أو هل يمكن أن تكون tags فارغة، أو هل تحمل createdAt منطقة زمنية دائمًا. ولا يعرف حتى أن id قد يكون نصًا في سجلات أخرى.
فالتوقع الصحيح هو: المولّد يوفر الكتابة، والإنسان يقدم المعنى.
2. الحقول الاختيارية وnull: ? ليس | null
هذا هو الجزء من الناتج المولّد الذي يُنسخ أكثر من غيره، والجزء الذي ينكسر أكثر من غيره.
| صيغة TypeScript | المعنى | JSON المطابق |
|---|---|---|
email: string |
حاضر دائمًا، نص دائمًا | {"email":"a@b.c"} |
email?: string |
قد يغيب المفتاح؛ وعند حضوره لا يمكن أن يكون null | {} أو {"email":"a@b.c"} |
email: string | null |
المفتاح حاضر دائمًا، والقيمة قد تكون null | {"email":null} |
email?: string | null |
قد يغيب المفتاح وقد تكون القيمة null | الحالات الثلاث كلها |
رأى المولّد عينة واحدة، فيُخرج عادةً الصيغة الأولى. وإن كانت API قادرة فعلًا على إرجاع null، فالنوع يكذب: كل شيء يُصرَّف، ثم يرمي email.toLowerCase() في وقت التشغيل.
هناك طريقتان لإصلاحه:
- اجمع عدة عينات حقيقية تغطي "المفتاح غائب" و"القيمة null" معًا، ثم دع المولّد يستنتج من جديد.
- بعد التوليد، غيّر الحقول يدويًا إلى
?: T | nullوعالج الفرعين فعلًا في الشيفرة.
ولا تأخذ الطريق المختصر بتعليم كل شيء ?: string. فالاختياري يعني "قد لا يوجد المفتاح" لا "قد تكون القيمة فارغة". كذلك يختلف سلوك undefined وnull في JSON.stringify: يُحذف الأول ويُحفظ الثاني كـ null.
3. عناصر المصفوفة غير المتساوية وأنواع الاتحاد
المصفوفات أسهل بنية يُخطئ فيها. فإن ظهر tags كـ ["admin"] و[] و["beta", 3] عبر ثلاثة سجلات، فقد يُخرج المولّد string[] أو (string | number)[] أو unknown[]، حسب العينات التي رآها.
وعلى الإنسان أن يقرر:
- أنواع العناصر مختلطة فعلًا: اكتب
(string | number)[]وضيِّق عند نقطة الاستخدام. - سجل واحد فقط قذر: أصلح البيانات. ولا تخفيه بـ
any[]. - المصفوفات الفارغة: لا يستطيع المولّد استنتاج نوع عنصر ويسقط إلى
never[]أوany[]، فعليك ملؤه. - الصفوف الثابتة الطول: استخدم
[string, string]فقط عندما يضمن العمل الطول فعلًا؛ وإلا فإضافة عنصر لاحقًا تصبح خطأ تصريف.
وتحتاج مصفوفات الكائنات انتباهًا أيضًا عندما تكون المفاتيح غير متساوية:
interface OrderItem {
sku: string;
quantity: number;
discount?: number; // some records do not carry this field
}
ضع الاختيارية على نوع العنصر لا على المصفوفة. فـ items?: OrderItem[] يعني أن المصفوفة نفسها قد تغيب، وهذا كلام مختلف تمامًا.
4. التواريخ والأعداد الكبيرة والتعدادات: أنواع لا يمتلكها JSON
لا يمتلك JSON إلا نصوصًا وأعدادًا؛ والمعنى العملي يعيش في الاصطلاحات. ويستنتج المولّد من الحرفيات، فصحّحه يدويًا:
| حرفية JSON | الناتج المولّد المعتاد | ما يجب أن تكون |
|---|---|---|
"2026-10-31T08:00:00Z" |
string |
أبقِ string في طبقة النقل وحوّل عند نقطة الاستخدام؛ ولا تعلن Date مباشرة أبدًا |
1717075200000 |
number |
استخدم number للطابع الزمني بالميلي ثانية ووثّق الوحدة |
"1234567890123456789" |
string |
أبقِه string حتى لا يفقد number الدقة |
1 و"1" مختلطان |
number | string |
طبّع إلى صيغة واحدة؛ ولا تدفع الاتحاد إلى شيفرة الأعمال |
"admin" |
string |
اتحاد حرفي 'admin' | 'editor'، أو ثابت تعداد مشترك |
true و"true" مختلطان |
boolean | string |
أصلح مصدر البيانات بدلًا من توليد هذا النوع |
القاعدة الأهم: التواريخ في JSON نصوص دائمًا. وكتابة createdAt: Date خداع ذاتي شائع — فـ JSON.parse لا ينشئ Date أبدًا، فتبقى القيمة في وقت التشغيل نصًا ويرمي .getTime(). أبقِ نوع النقل string، واستدعِ new Date(value) صراحةً عند الحدود، وتحقق بـ Number.isNaN.
والأعداد الكبيرة تعمل بالطريقة نفسها. فبمجرد أن يتجاوز id قيمة 2^53 يكون JSON.parse قد فقد الدقة بالفعل، وتوليد number يثبّت الخطأ في النوع فقط. الخلفية ترجع نصًا، والواجهة الأمامية تبقيه نصًا؛ وهذا الخيار الموثوق الوحيد.
5. الأنواع المولّدة لا تقدم تحققًا في وقت التشغيل
تُحذف أنواع TypeScript في وقت التصريف. فمهما كانت الـ interface دقيقة، لا تستطيع منع استجابة من نوع {"id": "x"} — فهي تقيّد شيفرتك أنت لا البايتات الواصلة عبر الشبكة.
/**
* Check whether a runtime value is a user object.
* @param value The runtime value to validate
* @returns Whether it satisfies the minimum shape of User
*/
function isUser(value: unknown): value is User {
if (typeof value !== 'object' || value === null) {
return false;
}
const candidate = value as Record<string, unknown>;
return typeof candidate.id === 'number' && typeof candidate.email === 'string';
}
لا يغطي حارس النوع إلا الحقول التي تكتبها في الفحص. ومع تكاثر الحقول يصبح الحرّاس المكتوبون يدويًا تعريفًا ثانيًا يحتاج صيانة، فينحرفون عن الـ interface.
والمقايضة العملية تبدو هكذا:
| النهج | تحقق في وقت التشغيل | مصدر الأنواع | يناسب |
|---|---|---|---|
| Interface فقط | لا شيء | مكتوبة يدويًا أو مولّدة | بيانات داخلية موثوقة، قيود وقت التصريف |
| Interface مع حارس مكتوب يدويًا | نعم | تعريفان ينحرفان | حقول قليلة، وحدود واضحة |
| zod / valibot | نعم | أنواع مستنتجة من المخطط | API خارجية، ونماذج، وإعدادات |
| JSON Schema مع مدقق | نعم | أنواع مولّدة من المخطط | عقود متعددة اللغات، وتوثيق |
إن كانت البيانات من خارج النظام، فاجعل المخطط مصدر الحقيقة الوحيد واشتق الأنواع بـ z.infer أو json-schema-to-ts. عندئذٍ "نجح التحقق" و"النوع صحيح" حقيقة واحدة، ولا يوجد تعريف ثانٍ ينحرف. ولمقاربة المزامنة ثنائية الاتجاه الكاملة راجع مزامنة Schema وأنواع TypeScript.
6. Interface أم type: لا تجادل حول الناتج المولّد
يُخرج المولّدون عادةً interface، لكن الاثنين قابلان للتبادل في معظم الحالات. والمهم فعلًا ثلاث قدرات:
- دمج الإعلانات: يمكن إعلان
interfaceعدة مرات فيُدمج تلقائيًا؛ ولا يمكن ذلك فيtype. وهذا يفيد عند ترقيع أنواع طرف ثالث، لكن نماذج الأعمال يجب أن تتجنبه — فالدمج يسمح بعبور "حقول إضافية" على المُصرِّف دون ملاحظة. - الاتحادات والأنواع المعيّنة: يستطيع
typeالتعبير مباشرةً عنA | BوPartial<T>وأنواع الحرفيات القالبية؛ ولا يستطيعinterfaceذلك. - الاستدعاء الذاتي: يدعمه الاثنان، لكن الأشكال التكرارية المعقدة تُقرأ طبيعيًا أكثر بوصفها
type.
وقاعدة فريق أنفع: استخدم interface لأشكال الكائنات، وtype للاتحادات وأنواع الأدوات وأنواع الدوال. واترك الناتج المولّد على افتراضه، واصرف وقت المراجعة على الاختيارية والتحقق في وقت التشغيل بدلًا من ذلك.
7. سلسلة الأدوات: ثلاث طرق للتوليد
| النهج | المُدخل | القوة | القيد |
|---|---|---|---|
| من JSON إلى TypeScript على الموقع | عينة JSON | يعمل فورًا، وتُعالَج محليًا | يعتمد على العينة الحالية فقط؛ ويلزم التضييق اليدوي |
| quicktype | JSON / JSON Schema | ناتج متعدد اللغات، وتكامل مع CLI | استنتاج محافظ، وقد تكون الاتحادات عريضة جدًا |
| json-schema-to-typescript | JSON Schema | مدفوع بالعقد وقابل للتكرار | يتطلب مخططًا موثوقًا أولًا |
استدعاء quicktype نموذجي:
npx quicktype -s json -o user.ts user.json --just-types
يُخرج --just-types الأنواع فقط دون شيفرة تحويل في وقت التشغيل، وهو يناسب المشاريع التي تملك طبقة تحليل بالفعل. وإن كان المخطط مكتوبًا يدويًا أو منتجًا من الخلفية، فـ json-schema-to-typescript أنسب لأنه يحترم required وenum وadditionalProperties — قيود لا يراها استنتاج العينات ببساطة.
ولتوليد المخطط مزالقه الخاصة، مثل تعليم كل حقل مطلوبًا أو تحويل null بصمت إلى اختياري؛ راجع مزالق مولّد JSON Schema الشائعة.
8. قائمة تحقق قبل إرسال الأنواع المولّدة
راجع هذه البنود الخمسة قبل إرسال الناتج:
- طبّع أسماء الحقول: لا تدع
userIdوuser_idيتعايشان؛ عيّن مرة واحدة في طبقة تحويل. - أعد فحص الاختيارية: قارن باستجابات حقيقية واعرف أي الحقول قد تغيب أو تكون
null. - أبقِ التواريخ والمعرّفات نصوصًا: وثّق الوحدة والصيغة في النوع، وحلّل صراحةً عند نقطة الاستخدام.
- قلّل
anyوunknown: في كل مكان أخرج فيه المولّدany، إما أن تضيف نوعًا أو تبقيهunknownوتضيف حارسًا. - أضف تحققًا في وقت التشغيل: تحقق من البيانات الخارجية مرة على الأقل؛ ولا تتوقع أن يلتقط interface بيانات سيئة.
إن كان المشروع يستخدم عدة مكتبات JSON (JSON.parse وsuperjson وlossless-json)، فسلوكها مع الأعداد الكبيرة والتواريخ يختلف، فتأكد منه أثناء الاختيار. راجع اختيار مكتبة تحليل JSON.
لماذا يعلّم المولّد كل حقل اختياري كمطلوب؟
رأى عينة واحدة، فكل مفتاح ظهر يُعامل كأنه حاضر دائمًا. قدّم عينات متعددة تغطي حالات مختلفة، أو ولّد من JSON Schema — فمصفوفة required هناك هي المصدر المرجعي.
لماذا يفشل إعلان Date في interface في وقت التشغيل؟
لا ينتج JSON.parse إلا نصوصًا وأعدادًا وقيمًا منطقية وnull وكائنات ومصفوفات؛ ولا ينشئ Date أبدًا. وإعلان Date وعد في وقت التصريف بينما تبقى القيمة في وقت التشغيل نصًا. أبقِ string في طبقة النقل وحوّل عند الحدود.
هل يمكن استخدام الأنواع المولّدة كتحقق؟
لا. تُحذف أنواع TypeScript بعد التصريف ولا تفرض شيئًا على بيانات وقت التشغيل. تحقق من البيانات الخارجية بـ zod أو valibot أو ajv أو حارس أنواع مكتوب يدويًا.
هل أعلن id كبيرًا صحيحًا بوصفه number أم string؟
إن كان قادرًا على تجاوز 2^53، فيجب أن يكون string. فـ JSON.parse يفقد الدقة أثناء التحليل، وإعلانه number يكتب الخطأ في النوع حيث لا يمكن إصلاحه بعد ذلك.