JSON Toolkit

من JSON إلى TypeScript: واجهات interface وحرّاس الأنواع وحدود التوليد

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

متوسطقراءة نحو 9 دقيقةنُشر في 2026-10-31حُدِّث في 2026-11-04
غلاف دليل JSON إلى TypeScript interface وحرّاس الأنواع

الخلاصة: ولّد 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() في وقت التشغيل.

هناك طريقتان لإصلاحه:

  1. اجمع عدة عينات حقيقية تغطي "المفتاح غائب" و"القيمة null" معًا، ثم دع المولّد يستنتج من جديد.
  2. بعد التوليد، غيّر الحقول يدويًا إلى ?: 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. قائمة تحقق قبل إرسال الأنواع المولّدة

راجع هذه البنود الخمسة قبل إرسال الناتج:

  1. طبّع أسماء الحقول: لا تدع userId وuser_id يتعايشان؛ عيّن مرة واحدة في طبقة تحويل.
  2. أعد فحص الاختيارية: قارن باستجابات حقيقية واعرف أي الحقول قد تغيب أو تكون null.
  3. أبقِ التواريخ والمعرّفات نصوصًا: وثّق الوحدة والصيغة في النوع، وحلّل صراحةً عند نقطة الاستخدام.
  4. قلّل any وunknown: في كل مكان أخرج فيه المولّد any، إما أن تضيف نوعًا أو تبقيه unknown وتضيف حارسًا.
  5. أضف تحققًا في وقت التشغيل: تحقق من البيانات الخارجية مرة على الأقل؛ ولا تتوقع أن يلتقط 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 يكتب الخطأ في النوع حيث لا يمكن إصلاحه بعد ذلك.