كيف تحدّد نوع استجابة API في TypeScript
تعطيك fetch بيانات من النوع any، وهذا يطفئ TypeScript في المكان الذي تأتي منه الأخطاء بالضبط. إليك كيف تمنح بيانات API نوعًا حقيقيًا، بسرعة وأمان.
يفحص TypeScript الكود الذي تكتبه، لكنه لا يستطيع فحص البيانات التي تصل وقت التشغيل. فلحظة استدعائك await response.json() تحصل على قيمة من النوع any، ومن تلك اللحظة يسمح لك المترجم بقراءة user.adress.city بخطأ إملائي دون أي اعتراض. وتحديد أنواع استجابات API يسدّ هذه الثغرة. يشرح هذا الدليل كيف تكتب الأنواع بسرعة انطلاقًا من عينة، وكيف تنمذج الأجزاء الصعبة، ومتى لا يكفي النوع وحده، وكيف تُبقي الأنواع متوافقة مع الواجهة حين تتغيّر.
الخطوة 1: استخرج الأنواع من عينة حقيقية
كتابة الواجهات (interfaces) يدويًا لاستجابة كبيرة عمل بطيء وعرضة للأخطاء. انسخ بدلًا من ذلك استجابة حقيقية، من تبويب Network في المتصفح مثلًا أو من توثيق الواجهة البرمجية، والصقها في تحويل JSON إلى TypeScript. استجابة كهذه:
{
"id": 1024,
"name": "Ada",
"tags": ["admin", "beta"],
"address": { "city": "London", "zip": "N1 9GU" },
"orders": [
{ "order_id": "A-17", "total": 42, "shipped_at": "2026-09-01T10:00:00Z" },
{ "order_id": "A-18", "total": 18.5, "shipped_at": null, "coupon": "WELCOME" }
]
}
تصبح، بعد ضبط اسم النوع الجذر على User:
export interface Address {
city: string;
zip: string;
}
export interface Order {
order_id: string;
total: number;
shipped_at: string | null;
coupon?: string;
}
export interface User {
id: number;
name: string;
tags: string[];
address: Address;
orders: Order[];
}
لاحظ ما استنتجه المولّد من الطلبين: كانت قيمة shipped_at فارغة (null) في أحد العنصرين، فصار نوعها string | null؛ وكان coupon غائبًا عن أحد العنصرين، فصار اختياريًا. وتحوّلت الكائنات المتداخلة إلى واجهات مستقلة لها أسماؤها، وصارت عناصر orders من النوع Order. ويجري كل ذلك داخل المتصفح، فالاستجابة التي تحمل بيانات عملاء حقيقية لا تُرسَل إلى أي مكان.
الخطوة 2: استخدم النوع مع fetch
أبسط طريقة لربط النوع بالبيانات دالة مساعدة صغيرة:
async function getJson<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return (await res.json()) as T;
}
const user = await getJson<User>('/api/users/1024');
user.address.city; // string, checked by the compiler
عبارة as T وعدٌ تقطعه للمترجم، لا فحص. فإن أرسل الخادم شيئًا آخر، فلن يلاحظ TypeScript ذلك. وفي الواجهات الداخلية التي تتحكم فيها، تكون هذه المقايضة مقبولة في الغالب؛ أما في غيرها، فتابع القراءة حتى الخطوة 4. والفائدة الكبرى هنا أن كل سطر يستخدم المتغير user بعد ذلك يحظى بالإكمال التلقائي في المحرر، وبتنبيه فوري إن أخطأت في اسم خاصية أو افترضت نوعًا غير صحيح.
الخطوة 3: نمذج الأجزاء الصعبة
| في JSON | في TypeScript | السبب |
|---|---|---|
| مفتاح يغيب أحيانًا | coupon?: string | اختياري: قد لا تكون الخاصية موجودة |
| قيمة تكون null أحيانًا | shipped_at: string | null | قابل لأن يكون null: الخاصية موجودة لكنها قد تكون فارغة |
| الحالتان معًا | note?: string | null | قد يحذفها الـ API أو يرسلها null |
| تاريخ | string | لا يوجد في JSON نوع للتاريخ؛ حوّله بـ new Date() حيث تحتاج |
| معرّف كبير | string إن كان الـ API يرسله بين علامتَي تنصيص | الأرقام الأكبر من 2^53 تفقد دقتها في JavaScript |
| مصفوفة فارغة في العينة | unknown[] | العينة لا تُظهر شكل العناصر |
فضّل unknown على any لكل ما لا تستطيع العينة إخبارك به: فـ unknown يفرض عليك فحص القيمة قبل استخدامها، بينما يطفئ any الفحص بصمت. والنوع المولَّد لا يكون أكمل من العينة التي وُلّد منها، لذا استخدم استجابة تتضمن الحقول الاختيارية، أو مصفوفة فيها عدة عناصر متنوعة.
الخطوة 4: تحقّق وقت التشغيل حين لا تكون البيانات بيانات مشروعك
واجهات الأطراف الثالثة تتغيّر، والخوادم الوسيطة (proxies) تعيد صفحات أخطاء بصيغة HTML، والعملاء القدامى يلتقون بخوادم جديدة. والفحص وقت التشغيل يلتقط هذه الحالات عند الحدّ الفاصل، برسالة خطأ واضحة، بدلًا من أن تظهر في عمق الواجهة. وهناك نهجان شائعان:
- JSON Schema: ولّد مخططًا من العينة نفسها بأداة مولد JSON Schema، وتحقّق به عبر مكتبة مثل Ajv. المخطط محايد لغويًا، فيستطيع خادم خلفي مكتوب بلغة أخرى استخدام الملف نفسه.
- أداة تحقّق مصمّمة لـ TypeScript أولًا: مكتبات مثل Zod تتيح لك وصف الشكل مرة واحدة واستنتاج نوع TypeScript منه، فلا يمكن أن يتباعد النوع والفحص أبدًا.
وأيًّا كان اختيارك، تحقّق مرة واحدة في الموضع الذي تدخل فيه البيانات إلى برنامجك، ودع بقية الكود يعتمد على النوع.
حين يتغيّر الـ API
الأنواع المولَّدة مرة واحدة تبتعد عن الواقع مع نموّ الواجهة البرمجية. احفظ العينة التي ولّدت منها الأنواع في المستودع بجوار الأنواع نفسها، حتى يتمكن من يأتي بعدك من إعادة توليدها ورؤية ما تغيّر في الفروق (diff). وإن كان الـ API ينشر وصف OpenAPI، فتوليد الأنواع منه أوثق من توليدها من العينات، لأنه يسرد كل حقل اختياري وكل قيمة ممكنة، ومنها تلك التي لا تُظهرها استجابة واحدة أبدًا. أما الواجهات التي لا وصف لها، فأداة التحقق وقت التشغيل من الخطوة 4 تحوّل عدم التطابق الصامت إلى خطأ تراه في السجلات.
interface أم type؟
لبيانات الـ API يصلح الاثنان. الواجهات (interface) يمكن توسيعها، وتُدمج إذا عُرّفت مرتين، وهذا ما تحبه بعض الفرق للأنواع العامة؛ أما الأسماء المستعارة للأنواع (type) فيمكنها أيضًا التعبير عن الاتحادات والأنواع المعيّنة (mapped types). اختر أسلوبًا واحدًا للمشروع والتزم به. والمولّد ينتج أيًّا منهما.
لغات أخرى من العينة نفسها
يمكن للعينة نفسها من JSON أن تصبح أيضًا هياكل Go أو نماذج Pydantic أو أصناف C# في تحويل JSON إلى أنواع، وهذا يساعد على إبقاء الواجهة الأمامية والخادم الخلفي على الشكل نفسه للبيانات. وإن كانت العينة نفسها معطوبة، فإن منسّق JSON يُظهر لك موضع الخطأ.