الصنف في TypeScript هو صنف JavaScript مع أنواع: كل حقل يُعرَّف مع نوعه في جسم الصنف، وتحصل التوابع ودالة البناء على معاملات وقيم مُعادة بأنواع محددة.
الناتج المترجم صنف JavaScript عادي حُذفت منه الأنواع، فكل ما ينطبق على أصناف JavaScript لا يزال ينطبق. ما تضيفه TypeScript يحدث وقت الترجمة: الاستدعاء الأخير يُرفض قبل أن يعمل البرنامج.
تعريف الحقول
يحتاج الحقل إلى تعريف في جسم الصنف قبل أن تتمكن دالة البناء من الإسناد إليه. الإسناد إلى this.owner دون تعريف owner هو الخطأ TS2339 (الخاصية غير موجودة). والحقل الذي له قيمة ابتدائية يُستنتج نوعه من القيمة، تمامًا مثل المتغير.
| التعريف | المعنى |
|---|---|
name: string; | يجب إسناده في دالة البناء |
count = 0; | له قيمة ابتدائية، ونوعه المستنتج number |
label?: string; | اختياري، ونوعه string | undefined |
readonly id: number; | يُسند مرة واحدة، ثم للقراءة فقط |
data!: string[]; | إسناد مؤكد: ثق بي، سيُضبط |
static total = 0; | ينتمي إلى الصنف، لا إلى النسخ |
مع تفعيل strict يتحقق strictPropertyInitialization من أن كل حقل غير اختياري يحصل على قيمة. هذا الصنف ينسى حقلين:
يبلّغ المترجم عن كليهما:
index.ts(3,5): error TS2564: Property 'name' has no initializer and is not definitely assigned in the constructor.
index.ts(4,5): error TS2564: Property 'age' has no initializer and is not definitely assigned in the constructor.
يوجد هذا الفحص لأنه بدونه سيمر new Profile().name.toUpperCase() في فحص الأنواع ثم ينهار بسبب undefined. أصلح كل حقل بإحدى أربع طرق: أعطه قيمة ابتدائية، أو أسنده في دالة البناء، أو اجعله اختياريًا بـ ?، أو اكتب name!: string حين يضبطه إطار عمل أو تابع init() لاحقًا. الطريقة الأخيرة تعطّل الفحص لذلك الحقل، فاستخدمها باعتدال.
التوابع وthis
تُحدَّد أنواع التوابع مثل الدوال. داخل التابع يكون نوع this هو نوع النسخة. يمكن للتابع أن يعيد this ليسمح بالتسلسل، ويتبع نوع القيمة المُعادة الأصناف الفرعية تلقائيًا.
class QueryBuilder {
private parts: string[] = [];
where(condition: string): this {
this.parts.push(condition);
return this;
}
build(): string {
return this.parts.join(" AND ");
}
}
const sql = new QueryBuilder().where("age > 18").where("active = 1").build();
console.log(sql); // age > 18 AND active = 1
يفقد التابع this حين تفصله عن الكائن، وهو خطأ كلاسيكي في JavaScript. لا تكتشفه TypeScript افتراضيًا، لكنها تكتشفه إن عرّف التابع معامل this. هذا المعامل يُحذف من الناتج ولا يحمي إلا موضع الاستدعاء:
ينجح حقل دالة السهم لأن كل نسخة تحصل على دالتها الخاصة التي تحتفظ بـ this. الثمن دالة لكل نسخة بدل تابع واحد مشترك على الـ prototype.
الـ Getters والـ Setters
تبدو الموصِّلات get وset من الخارج كأنها خصائص. الـ getter الذي ليس له setter للقراءة فقط: الإسناد إليه خطأ ترجمة (TS2540).
يرفض المترجم الإسناد، وإن تم تجاوز الفحص يرمي وقت التشغيل TypeError أيضًا، لأن الملف المترجم يعمل في الوضع الصارم (يضع المترجم "use strict" في أول الملف). أما RangeError في الـ setter ففحص وقت تشغيل كتبته بنفسك. النوع number يقول فقط إن القيمة رقم؛ ولا يعرف شيئًا عن النطاقات.
الأعضاء الثابتة
الحقول والتوابع static تنتمي إلى الصنف نفسه. تصل إليها عبر اسم الصنف، لا عبر نسخة. وتعمل كتلة static { } مرة واحدة عند تعريف الصنف.
لا تستطيع الأعضاء الثابتة استخدام معاملات النوع الخاصة بالصنف: في class Box<T> يكون static empty: T الخطأ TS2302، لأنه لا يوجد إلا حقل ثابت واحد يتشاركه كل من Box<string> وBox<number> وغيرهما.
تطبيق Interface
يطلب implements من المترجم التحقق من أن الصنف يملك كل ما يتطلبه الـ interface. العضو الناقص هو الخطأ TS2420: Class 'X' incorrectly implements interface 'Y'.
يمكن للصنف تطبيق عدة interfaces: class Doc implements Printable, Serializable. وهناك أمران لا يفعلهما implements. لا يحدد أنواع معاملات توابع الصنف نيابة عنك: check(s) { ... } داخل صنف يطبّق check(s: string): boolean لا يزال الخطأ TS7006 (Parameter 's' implicitly has an 'any' type)، فصِف أنواعها. ولا يترك أي أثر وقت التشغيل، فلا يمكن كتابة obj instanceof Shape.
private radius في دالة البناء خاصية معامل (parameter property): تعرّف الحقل وتسنده في خطوة واحدة. راجع دوال البناء.
الصنف نوع أيضًا
تعريف الصنف ينشئ شيئين باسم واحد: قيمة (دالة البناء التي تستدعيها بـ new) ونوعًا (شكل النسخة). ولأن TypeScript تقارن الأنواع بحسب البنية، فأي كائن له الأعضاء العامة نفسها يطابق النوع، حتى لو لم يُنشئه الصنف.
يجتاز الكائن العادي فحص النوع لكنه ليس Point وقت التشغيل: قيمة ({ x: 6, y: 8 }) instanceof Point هي false. الصنف الذي له عضو private أو #private يمنع ذلك: لا يمكن الإسناد إليه إلا من نسخ ذلك الصنف (أو أصنافه الفرعية).
أخطاء شائعة
- تعريف حقل وعدم إسناده أبدًا. يخبرك
TS2564أن الحقل سيكونundefined. أعطه قيمة ابتدائية بدل إسكاته بـ!. - تمرير تابع كـ callback.
button.onclick = obj.handleيفقدthis. استخدم حقل دالة سهم أوobj.handle.bind(obj). - توقّع أن يضيف
implementsكودًا أو أنواعًا. إنه يفحص فقط. وأنواع المعاملات لا تزال تحتاج إلى توصيف. - استخدام
instanceofمع interface. الـ interfaces غير موجودة وقت التشغيل؛ افحص صنفًا أو استخدم type guard. - افتراض أن الأنواع تتحقق من البيانات. حقل
numberيقبل أي رقم وقت الترجمة، وأي شيء على الإطلاق من البيانات غير محددة النوع وقت التشغيل.
الأسئلة الشائعة
كيف أنشئ صنفًا في TypeScript؟
عرّف كل حقل مع نوعه في جسم الصنف، ثم اكتب دالة البناء والتوابع كما في JavaScript: class User { name: string; constructor(name: string) { this.name = name; } greet(): string { return "Hi, " + this.name; } }. وأنشئ نسخة بـ new User("Ada").
ماذا تعني الرسالة "has no initializer and is not definitely assigned in the constructor"؟
إنها الخطأ TS2564 من strictPropertyInitialization (جزء من strict). حقل نوعه string سيبدأ بالقيمة undefined لأن لا شيء يضبطه. أصلحه بقيمة ابتدائية (name = "")، أو بإسناد في دالة البناء، أو بحقل اختياري (name?: string)، أو، حين يضبطه شيء خارج دالة البناء، بتأكيد الإسناد القاطع (definite assignment) (name!: string).
ما الفرق بين implements وextends في TypeScript؟
extends يرث الكود من صنف أب: حقوله وتوابعه موجودة في الصنف الابن وقت التشغيل. أما implements فيطلب من المترجم فقط التحقق من أن للصنف شكل interface معين. لا يضيف شيئًا إلى الصنف ويختفي من JavaScript الناتج.
هل يمكن استخدام الصنف كنوع في TypeScript؟
نعم. اسم الصنف قيمة (دالة البناء) ونوع (شكل نسخه) في آن واحد. يقبل let u: User أي كائن له الأعضاء العامة نفسها، لأن TypeScript تقارن الأنواع بنيويًا. وtypeof User هو نوع دالة البناء نفسها.
هل تختلف أصناف TypeScript عن أصناف JavaScript وقت التشغيل؟
لا. تترجم TypeScript الصنف إلى صنف JavaScript عادي. توصيفات الأنواع وعبارات implements والمعدّلات مثل private تُحذف؛ ولا يوجد وقت التشغيل إلا ميزات JavaScript مثل حقول #private وكتل static والـ getters.