في وصف شكل الكائن يؤدي interface وtype المهمة نفسها، ويمكن إسناد قيم أحدهما إلى الآخر عندما تتطابق الأشكال. الفروق عند الأطراف: يستطيع type تسمية أشياء لا تستطيعها الواجهة (union وtuples والأنواع المحسوبة)، وتستطيع الواجهة فعل بعض الأشياء التي لا يستطيعها type alias (الدمج، وextends المفحوص).
تقارن TypeScript أنواع الكائنات بحسب البنية، لذلك لا يهم اسم التصريح في قابلية الإسناد. وفيما يلي قائمة المواضع التي يهم فيها الاختيار.
جدول المقارنة
| الميزة | interface | type |
|---|---|---|
| أشكال الكائنات | نعم | نعم |
| الاختيارية وreadonly والدوال وindex signatures | نعم | نعم |
| الأنواع العامة | نعم | نعم |
Union (A | B) | لا | نعم |
| tuples والأنواع البدائية وأنواع الدوال وحدها | لا (أنواع الدوال كـ call signatures فقط) | نعم |
| mapped types والأنواع الشرطية | لا | نعم |
| التوسيع | extends، والتعارضات أخطاء | &، والتعارضات تصبح never |
| دمج التصريحات | نعم | لا (معرّف مكرر) |
قابل للإسناد إلى Record<string, T> | لا | نعم، عندما تناسب الخصائص |
implements في صنف | نعم | نعم، إذا كان نوع كائن |
| التعريفات التعاودية | نعم | نعم |
ما لا يستطيعه إلا type
كل ما ليس شكل كائن واحد يحتاج إلى type alias:
لا يمكن كتابة أي من هذه بـ interface (الأخيران مشروحان في mapped types والأنواع الشرطية). وهذا هو السبب العملي لاستخدام كل قاعدة كود type في مكان ما، أيًا كان ما تستخدمه لأشكال الكائنات.
ما لا تستطيعه إلا interface: دمج التصريحات
تصريحان interface بالاسم نفسه في النطاق نفسه يندمجان في واحد. أما تصريحان type بالاسم نفسه فهما الخطأ TS2300، Duplicate identifier.
interface Settings {
theme: string;
}
interface Settings {
fontSize: number;
}
const s: Settings = { theme: "dark", fontSize: 14 }; // needs both
type Options = { a: number };
type Options = { b: number }; // error TS2300: Duplicate identifier 'Options'.
الدمج هو طريقة توسيع أنواع المكتبات من الخارج: إضافة خاصية إلى Window العام، أو إلى Request في Express، أو إلى نوع السمة في مكتبة ما. إذا كنت تنشر أنواعًا قد يحتاج المستخدمون إلى توسيعها، فاستخدم الواجهات. وفي كود تطبيقك، يحدث الدمج غير المقصود (ملفا سكربت يعلنان اسم الواجهة العامة نفسه) بصمت ما لم يعلن الاثنان الخاصية نفسها بأنواع مختلفة، وهذه إحدى الحجج التي تقدمها بعض الفرق لتفضيل type.
extends مقابل Intersection
توسّع الواجهة بـ extends، ويوسّع type alias بـ &. ينتجان النتيجة نفسها في الغالب، لكنهما يتعاملان مع الخاصية المتعارضة بشكل مختلف. يبلّغ extends عن التعارض عند التصريح:
index.ts(7,11): error TS2430: Interface 'Broken' incorrectly extends interface 'Base'.
Types of property 'id' are incompatible.
Type 'number' is not assignable to type 'string'.
أما الـ intersection فيقبل التعارض نفسه بصمت ويحوّل الخاصية إلى never (نص هو رقم في الوقت نفسه). ولا يظهر الخطأ إلا لاحقًا، عندما تحاول إنشاء قيمة:
يشير الخطأ إلى الكائن، بعيدًا عن التصريح الذي تسبب فيه. لبناء أنواع كائنات من أنواع كائنات أخرى، يعطي extends رسالة أفضل.
Index Signatures: فرق يسهل إغفاله
يحصل type alias لنوع كائن على index signature ضمني، لذلك يمكن تمريره حيث يُتوقع Record<string, unknown>. أما الواجهة فلا. هذا مقصود: شرح فريق TypeScript (المسألة #15300 في مستودع TypeScript) هو أن الواجهة يمكن توسيعها بتصريحات لاحقة، فاستنتاج index signature لها أقل أمانًا، وتغيير القاعدة الآن سيكسر كودًا كثيرًا.
الاستدعاء المعلّم بـ @ts-expect-error يعمل رغم ذلك ويطبع حقول Alan: القاعدة وقت الترجمة. وهذا هو السبب المعتاد لخطأ محيّر عندما تُمرَّر قيمة واجهة إلى دالة مساعدة للتسجيل أو التسلسل أو الاستعلام نوع معاملها Record<string, ...>. حوّل ذلك التصريح الواحد إلى type، أو انشر القيمة، أو حدد نوع معامل الدالة المساعدة بواجهة أو بنوع عام بدلًا من ذلك.
الأداء ورسائل الأخطاء
تقترح صفحة الأداء في wiki الخاص بـ TypeScript (قسم «Preferring Interfaces Over Intersections») استخدام interface Foo extends Bar, Baz { ... } بدل type Foo = Bar & Baz & { ... } عند تركيب أنواع الكائنات. وأسبابها: الواجهة نوع كائن واحد مسطح يكتشف تعارضات الخصائص، والعلاقات بين الواجهات تُخزَّن مؤقتًا (أما intersections ككل فلا)، وفحص قيمة مقابل intersection يفحص كل مكوّن قبل النوع المسطح. يهم الفرق في قواعد الكود الكبيرة ذات الأنواع المركبة الكثيرة؛ أما مع بضعة أشكال كائنات عادية فلن تقيسه.
وتشير الصفحة نفسها إلى أن الواجهات تُعرض بشكل أفضل. تُعرض الواجهة باسمها في تلميحات التمرير ورسائل الأخطاء، بينما يُطبع الاسم المستعار لـ intersection غالبًا مفككًا إلى أجزائه، ما يجعل الرسائل الطويلة أصعب قراءة.
أيهما تستخدم
قاعدة تعمل:
- أشكال الكائنات:
interface. تعطيكextendsمفحوصًا، وأخطاء أوضح في التركيبات الكبيرة، وتتيح لمستخدمي المكتبة توسيعها. وهذا يطابق القاعدة العملية في دليل TypeScript: «استخدمinterfaceحتى تحتاج إلى ميزات منtype». - كل ما عدا ذلك:
type. union وtuples وأنواع الدوال والأنواع الحرفية، وكل ما يُبنى بـ mapped types أو الأنواع الشرطية أو template literal types. - الاستثناء: استخدم
typeلشكل كائن يجب أن يناسب معاملاتRecord<string, ...>، أو عندما تريد عمدًا منع الدمج.
استخدام type لكل شيء خيار متسق أيضًا، وتفعله قواعد كود كثيرة. أما الخلط العشوائي بين الاثنين فهو الخيار الوحيد الذي يجب تجنبه: يجعل القراء يتساءلون إن كان الفرق مقصودًا.
الأسئلة الشائعة
ما الفرق بين type و interface في TypeScript؟
كلاهما يصف أشكال الكائنات ويمكن استبدال أحدهما بالآخر لهذا الغرض. يستطيع type أيضًا تسمية union وtuples والأنواع البدائية وmapped types والأنواع الشرطية، وهو ما لا يستطيعه interface. ويدعم interface دمج التصريحات وextends الذي يتحقق من الخصائص المتعارضة. وأنواع الكائنات المكتوبة بـ type يمكن إسنادها أيضًا إلى أنواع index signature مثل Record<string, unknown>، بينما لا يمكن ذلك مع الواجهات.
هل أستخدم type أم interface؟
القاعدة العملية في دليل TypeScript هي: استخدم interface حتى تحتاج إلى ميزة لا يملكها إلا type. عمليًا يعني ذلك الواجهات لأشكال الكائنات وtype لـ union وtuples وأنواع الدوال والأنواع المحسوبة. والفرق التي تستخدم type لكل شيء تنجح أيضًا؛ المهم قاعدة واحدة ثابتة.
هل interface أسرع من type في TypeScript؟
في تركيب أنواع الكائنات، نعم في بعض الحالات. توصي إرشادات الأداء من فريق TypeScript بـ interface ... extends بدل intersections الكبيرة (A & B & { ... })، لأن العلاقات بين الواجهات تُخزَّن مؤقتًا والواجهة نوع واحد مسطح. أما لشكل كائن بسيط فلا فرق يُذكر.
هل يمكن لصنف أن يطبّق type alias؟
نعم، إذا كان الاسم المستعار نوع كائن (أو intersection من أنواع كائنات): class Point implements PointType { ... } يعمل. لا يستطيع الصنف تطبيق نوع union؛ هذا هو الخطأ TS2422.
هل يمكن لـ interface أن توسّع type alias؟
نعم، ما دام الاسم المستعار نوع كائن: type Base = { id: string }; interface User extends Base { name: string } صحيح. ولا يمكنها توسيع اسم مستعار لـ union. وفي الاتجاه الآخر، يمكن لـ type alias البناء على واجهة باستخدام &.