القواعد التالية هي التي تمنع أكبر عدد من الأخطاء في كود TypeScript الحقيقي. كل واحدة تعرض النسخة الشائعة أولًا ثم النسخة الأفضل، في كود يمكنك تشغيله. القاعدة الأولى هي الأهم: توقّف عن استخدام any.
استخدم unknown بدلًا من any
يعطّل any فحص الأنواع للقيمة ولكل ما يُحسب منها. أما unknown فيقبل أيضًا أي قيمة، لكن عليك فحصها قبل استخدامها، وهذا يضع الفحص حيث تدخل البيانات إلى برنامجك:
الحدود هي الأماكن التي تتوقف فيها الأنواع عن كونها مضمونة: JSON.parse، واستجابات fetch، وlocalStorage، ومدخلات النماذج، ومتغيرات البيئة، والرسائل القادمة من عمليات أخرى. تحقق هناك بـ type guard أو بمكتبة مخططات، ويستطيع بقية الكود الوثوق بأنواعه.
أبقِ strict مفعّلًا
strict هو الإعداد الافتراضي في TypeScript 7؛ لا توقفه. وأضف الفحوص التي يتركها والتي تكتشف أكثر الأخطاء:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true
}
}
يجعل noUncheckedIndexedAccess نوع arr[i] وrecord[key] يتضمن undefined، وهو ما تعيده فعلًا عند فهرس غير موجود. ويجعل noImplicitOverride دالة الصنف الفرعي تصرّح بـ override، ويرفض noFallthroughCasesInSwitch أي case يمتد تنفيذه إلى الذي يليه.
دع الاستنتاج يعمل
حدد ما لا يستطيع TypeScript معرفته: معاملات الدوال، وأنواع الإعادة للدوال التي تستخدمها وحدات أخرى. واترك المتغيرات المحلية ومعاملات دوال الاستدعاء للاستنتاج. تحديد النوع غير الضروري ليس مجرد ضجيج؛ فقد يجعل النوع أوسع من القيمة:
دون التعليق @ts-expect-error يكون setStatus(annotated) الخطأ TS2345. أما الـ const المستنتج فيحتفظ بالنوع الحرفي "active"، فيُقبل. مرّر المؤشر فوق متغير في محررك لترى ما استُنتج قبل أن تضيف نوعًا.
فضّل أنواع Union على Enum
الـ union من نصوص حرفية يعطي الإكمال التلقائي والفحص الشامل دون أي كود مولَّد. وعندما تحتاج أيضًا إلى قائمة القيم وقت التشغيل، اشتق النوع من مصفوفة as const:
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"
function canEdit(role: Role): boolean {
return role !== "viewer";
}
console.log(canEdit("editor")); // true
console.log(ROLES.filter(canEdit)); // [ 'admin', 'editor' ]
canEdit("owner"); // error TS2345: Argument of type '"owner"' is not assignable to parameter of type '"admin" | "editor" | "viewer"'.
يُترجم enum Role { Admin, Viewer } إلى كائن فيه ربط عكسي، والمعامل من نوع enum رقمي يقبل أي متغير number، حتى لو كانت قيمته غير مذكورة في الـ enum. ولا تعمل الـ enums أيضًا مع حذف الأنواع في Node (TypeScript enum is not supported in strip-only mode). المقارنة بين الخيارين موجودة في صفحة الـ enums.
افحص كائنات الإعدادات بـ satisfies
تحديد نوع كائن بنوع واسع مثل Record<string, Route> يفحص قيمه لكنه ينسى مفاتيحه. أما satisfies فيفحص الشيء نفسه ويحتفظ بالنوع الدقيق:
استخدم تحديد النوع عندما يجب أن يكون للمتغير النوع المصرّح به بالضبط (معامل دالة، قيمة ستعيد إسنادها). واستخدم satisfies لجداول البحث، وخرائط المسارات، ورموز السمات (theme tokens)، والكائنات الثابتة الأخرى.
صمّم الحالة بـ Discriminated Unions
الكائن الواحد ذو الحقول الاختيارية يسمح بحالات لا يمكن أن تحدث: loading: true مع error معًا، أو غياب data بعد النجاح. أما union من كائنات تشترك في وسم واحد فلا يسمح إلا بالحالات الحقيقية، ولا يرى كل فرع إلا حقوله:
سطر never هو الفحص الشامل. عندما يضيف أحدهم حالة وينسى معالجتها، يفشل البناء عند ذلك السطر:
الخطأ هو index.ts(14,13): error TS2322: Type '{ status: "cancelled"; }' is not assignable to type 'never'. وهو يسمّي الحالة غير المعالجة. أضف case "cancelled": فيُترجم الكود.
تجنب ! وas
تأكيد عدم القيمة الفارغة x! وتأكيد النوع x as T يطلبان من المترجم التوقف عن الفحص. لا يغيّر أي منهما القيمة وقت التشغيل، لذلك يصبح التأكيد الخاطئ انهيارًا لاحقًا، بعيدًا عن سببه:
استبدل ! بـ ?. أو ?? أو return مبكر أو خطأ مرمي برسالة مفيدة. واستبدل as بـ type guard يختبر القيمة فعلًا. التأكيد الوحيد الآمن دائمًا هو as const، لأنه يجعل النوع أضيق وللقراءة فقط لا أكثر. وإعداد جيد لأداة الفحص (القاعدتان no-non-null-assertion وno-explicit-any في typescript-eslint) ينبّه إلى البقية.
اجعل البيانات readonly
ميّز الخصائص والمصفوفات بـ readonly عندما لا ينبغي للكود تغييرها. عندها يرفض المترجم push وsort والإسناد، وتعيد الدوال قيمًا جديدة بدلًا من تعديل مدخلاتها:
يُفحص readonly وقت الترجمة فقط وعلى مستوى واحد فقط: لا يجمّد الكائن وقت التشغيل. ومع ذلك يكفي هذا لاكتشاف التعديل غير المقصود للحالة المشتركة، وهو سبب معظم هذه الأخطاء.
الأسئلة الشائعة
هل أستخدم any في TypeScript؟
نادرًا جدًا في كود التطبيقات. any يعطّل الفحص للقيمة ولكل ما يُشتق منها. استخدم unknown للقيم التي لا تعرف نوعها بعد، وضيّقها بالفحوص؛ واحتفظ بـ any لمخارج الطوارئ النادرة، مع تعليق يشرح السبب.
هل أحدد نوع كل متغير في TypeScript؟
لا. دع TypeScript يستنتج المتغيرات المحلية ومعاملات دوال الاستدعاء. حدد أنواع معاملات الدوال (لا يمكن استنتاجها)، وأنواع الإعادة للدوال المصدَّرة، حتى لا يغيّر تعديلٌ داخل الدالة نوعها العام بصمت.
هل استخدام enum في TypeScript ممارسة سيئة؟
ليس خطأ، لكن فرقًا كثيرة تتجنبه. الـ enum يولّد كودًا وقت التشغيل، ولا يعمل مع حذف الأنواع في Node، والمعامل من نوع enum رقمي يقبل أي متغير number مهما كانت قيمته. أما union من نصوص حرفية، أو مصفوفة as const مع نوع مشتق منها، فيعطي الإكمال التلقائي والفحوص نفسها دون أي كود مولَّد.
متى أستخدم تأكيدات النوع بـ as؟
فقط عندما تعرف شيئًا لا يستطيع المترجم معرفته، ويُفضّل أن يكون ذلك مباشرة بعد فحص يثبته. لا يغيّر as أي قيمة وقت التشغيل، لذلك يُترجم {} as User ثم لا يكون فيه name. والـ type guard الذي يختبر القيمة هو الأداة الأكثر أمانًا في معظم الحالات.
ما أفضل إعدادات tsconfig لمشروع TypeScript جديد؟
أبقِ strict مفعّلًا (الإعداد الافتراضي في TypeScript 7) وأضف noUncheckedIndexedAccess. وكثير من المشاريع تفعّل أيضًا noImplicitOverride وnoFallthroughCasesInSwitch وverbatimModuleSyntax. والإعداد الذي يكتبه tsc --init يضبط strict وnoUncheckedIndexedAccess وexactOptionalPropertyTypes وverbatimModuleSyntax، وغيرها.