ينشئ interface Child extends Parent واجهة جديدة فيها كل أعضاء Parent إضافة إلى أعضائها. ويمكن استخدام قيم النوع الابن حيثما يُتوقع الأب، لأنها تملك كل خصائصه.
تظهر الكلمة نفسها في أربعة مواضع في TypeScript، بمعانٍ متقاربة لكنها مختلفة:
| الموضع | مثال | المعنى |
|---|---|---|
| Interface | interface Dog extends Animal {} | وراثة أعضاء الأب |
| Class | class Dog extends Animal {} | وراثة الحقول وتطبيقات الدوال (JavaScript وقت التشغيل) |
| معامل نوع عام | <T extends Animal> | قيد: يجب أن يكون وسيط النوع قابلًا للإسناد إلى Animal |
| نوع شرطي | T extends string ? A : B | اختبار: هل T قابل للإسناد إلى string؟ |
توسيع عدة واجهات
اسرد عدة آباء بعد extends مفصولين بفواصل. يملك الناتج أعضاءها جميعًا:
interface Identified {
id: number;
}
interface Timestamped {
createdAt: Date;
updatedAt: Date;
}
interface Post extends Identified, Timestamped {
title: string;
}
// Post requires id, createdAt, updatedAt and title
إذا أعلن أبوان الخاصية نفسها بنوعين غير متطابقين، يكون الابن هو الخطأ TS2320 (Interface 'Post' cannot simultaneously extend types 'A' and 'B')، ما لم يُعد الابن تعريف تلك الخاصية بنوع قابل للإسناد إلى الاثنين.
توسيع type alias
لا يملك الاسم المستعار type عبارة extends. تبني عليه باستخدام intersection، &، الذي يجمع أعضاء الطرفين. ويمكن للواجهة أيضًا توسيع type alias لنوع كائن مباشرة، ويمكن لـ type alias أن يتقاطع مع واجهة: النوعان يختلطان بحرية.
يتصرف الطريقان بالطريقة نفسها حتى تتعارض الخصائص: يبلّغ extends عن الخاصية المتعارضة عند التصريح، بينما يحوّلها & بصمت إلى never. المقارنة موجودة في صفحة interface مقابل type، وتفاصيل & في صفحة intersection types.
تجاوز أنواع الخصائص
يمكن للواجهة الابن إعادة تعريف خاصية من الأب، لكن فقط بنوع قابل للإسناد إلى نوع الأب. تضييق string إلى قيمة حرفية، أو حذف null من union، مسموح. أما تغيير النوع إلى شيء غير ذي صلة فلا:
index.ts(13,11): error TS2430: Interface 'Label' incorrectly extends interface 'Shape'.
Types of property 'kind' are incompatible.
Type 'number' is not assignable to type 'string'.
توجد القاعدة حتى يظل ممكنًا استخدام Label في كل مكان يُتوقع فيه Shape. لو كان kind يمكن أن يكون رقمًا، لتعطل كود مكتوب لـ Shape يستدعي shape.kind.toUpperCase().
استبدال الخصائص باستخدام Omit
عندما تحتاج فعلًا إلى نوع مختلف لخاصية موروثة، احذفها من الأب أولًا بـ Omit، ثم أعلن الخاصية الجديدة. حالة شائعة هي تحويل بيانات API، حيث تصل المعرّفات والتواريخ كنصوص:
Omit<ApiUser, "id" | "createdAt"> هو { name: string }، لذلك لا تتعارض id وcreatedAt الجديدتان مع أي شيء. ويمكن توسيع Pick وPartial وبقية أنواع الأدوات بالطريقة نفسها.
extends مع الأصناف
class Dog extends Animal هو وراثة الأصناف في JavaScript، وعلى خلاف extends في الواجهات فهو موجود وقت التشغيل: يحصل Dog على مُنشئ Animal وحقوله وتطبيقات دواله، ويكون new Dog(...) instanceof Animal مساويًا لـ true. وتضيف TypeScript فحوص أنواع فوق ذلك، مثل الكلمة المفتاحية override:
يستطيع الصنف أن يوسّع (extends) صنفًا واحدًا فقط، لكنه يستطيع أن يطبّق (implements) أي عدد من الواجهات. لا ينسخ implements شيئًا: يتحقق فقط من أن الصنف يعلن ما تتطلبه الواجهة. super والأعضاء المحمية وoverride مشروحة في الوراثة.
extends في الأنواع العامة والأنواع الشرطية
داخل الأقواس الزاوية، extends قيد. يقبل <T extends { length: number }> أي وسيط نوع يملك length رقمية، ويتيح لجسم الدالة استخدام .length. وفي النوع الشرطي، يسأل T extends U ? X : Y هل T قابل للإسناد إلى U.
في الموضعين تعني extends «قابل للإسناد إلى»، وهي العلاقة نفسها التي تفحصها صيغة الواجهات. القيود مشروحة بعمق في قيود الأنواع العامة، والاختبارات على مستوى الأنواع في الأنواع الشرطية.
الأسئلة الشائعة
كيف أوسّع interface في TypeScript؟
اكتب interface Child extends Parent { ... }. تملك الواجهة الابن كل خصائص الأب إضافة إلى ما تعلنه. ولتوسيع عدة واجهات دفعة واحدة، افصل بينها بفواصل: interface C extends A, B { ... }.
كيف أوسّع type alias في TypeScript؟
لا يستطيع type alias استخدام extends؛ اجمع الأنواع بـ intersection بدلًا من ذلك: type Admin = User & { permissions: string[] }. ويمكن للواجهة أيضًا توسيع type alias لنوع كائن مباشرة: interface Admin extends User { permissions: string[] }.
هل يمكنني تجاوز نوع خاصية عند توسيع interface؟
فقط بنوع متوافق أضيق: يمكن أن يحل kind: "circle" محل kind: string. أما النوع المختلف، مثل number لخاصية string، فهو الخطأ TS2430 (Interface 'X' incorrectly extends interface 'Y'). لاستبدال خاصية بنوع غير ذي صلة، احذفها أولًا: interface User extends Omit<ApiUser, "id"> { id: number }.
ما الفرق بين extends و implements؟
extends يرث: تحصل الواجهة على أعضاء الأب، ويحصل الصنف على حقول الصنف الأب وتطبيقات دواله. أما implements فيفحص فقط: الصنف الذي يطبّق واجهة يجب أن يعلن كل عضو بنفسه، ولا يكسب شيئًا من الواجهة.
ماذا يعني extends في نوع عام مثل <T extends string>؟
إنه قيد لا وراثة. T extends string يعني أن وسيط النوع يجب أن يكون قابلًا للإسناد إلى string، فيمكنك داخل الدالة استخدام دوال النصوص على T. وفي النوع الشرطي، T extends U ? X : Y اختبار: إذا كان T قابلًا للإسناد إلى U فالناتج X، وإلا Y.