Menu
flag Ar iconالعربيةdown icon

الواجهات interface في TypeScript: الصياغة والخصائص والأمثلة

تسمّي الواجهة (interface) شكل الكائن: الخصائص التي يملكها والأنواع التي تحملها. تعرّف على طريقة تعريفها، والخصائص الاختيارية وreadonly، والدوال، وindex signatures، والتوسيع، والتطبيق في صنف، ودمج التصريحات، والواجهات العامة، وطريقة إعطاء الواجهة قيمًا افتراضية.

تحتوي هذه الصفحة على محررات قابلة للتشغيل - حرّر، شغّل، وشاهد النتيجة فوراً.

تعطي الواجهة (interface) اسمًا لشكل الكائن: الخصائص التي يجب أن يملكها ونوع كل منها. بعد تعريفها تستخدم الاسم كنوع، ويفحص المترجم مقابله كل كائن تمرره أو تعيده أو تسنده.

الاستدعاء الأخير خطأ الترجمة TS2741. تُمحى الواجهات عند ترجمة الكود: لا أثر لـ User في ناتج JavaScript، ولا شيء يفحص الشكل وقت التشغيل.

تعريف interface

الصياغة هي الكلمة interface، ثم اسم (بصيغة PascalCase عرفًا)، ثم جسم يسرد الأعضاء. يمكن فصل الأعضاء بفواصل منقوطة أو فواصل أو مجرد فواصل أسطر؛ والفاصلة المنقوطة الأسلوب الشائع.

interface Product {
  sku: string;               // required property
  price: number;
  tags: string[];            // array property
  dimensions: {              // nested object type
    width: number;
    height: number;
  };
  discount?: number;         // optional property
  readonly createdAt: Date;  // cannot be reassigned
  label(): string;           // method
}

الواجهة نوع لا قيمة. لا يمكن إنشاء نسخة منها بـ new، وليس لها قيم افتراضية، وobj instanceof Product هو الخطأ TS2693 ('Product' only refers to a type, but is being used as a value here). لفحص شكل وقت التشغيل، اكتب type guard.

الأنواع البنيوية وفحص الخصائص الزائدة

تقارن TypeScript الأشكال لا الأسماء. أي كائن يملك الخصائص المطلوبة يناسب الواجهة، سواء عُرّف بها أم لا. والخصائص الإضافية مقبولة، باستثناء واحد: الكائن الحرفي المكتوب مباشرة حيث تُتوقع الواجهة يخضع لفحص الخصائص الزائدة، لأن المفتاح المجهول هناك خطأ إملائي في أغلب الأحيان.

هذا الخطأ (TS2353) هو ما يكتشف { id: 1, name: "a", emial: "x" } لنوع User: بل ويقترح المترجم Did you mean to write 'email'? (TS2561).

الخصائص الاختيارية وreadonly

علامة ? بعد الاسم تجعل الخاصية اختيارية: يمكن للكائن أن يتركها، وقراءتها تعطي T | undefined. ويمنع readonly إعادة إسناد الخاصية بعد إنشاء الكائن.

يظهر هنا حدّان. الأول أن readonly وقت الترجمة فقط: السطران المعلّمان بـ @ts-expect-error يعملان عند الضغط على Run وينجحان، والأسطر الأخيرة تغيّر apiUrl عبر مرجع نوعه بلا readonly. والثاني أنه سطحي: readonly hosts: string[] كان سيمنع إعادة إسناد hosts لكنه يسمح بـ hosts.push(...)، ولهذا حُدد نوع المصفوفة نفسها بـ readonly string[]. إنه يوثّق النية ويفرضها في الكود ذي الأنواع؛ ولا يجمد شيئًا. ونوع الأداة Readonly<T> يجعل كل خصائص واجهة موجودة readonly دفعة واحدة.

الدوال وخصائص الدوال

يمكن كتابة الدالة كـ method signature، name(params): ReturnType، أو كخاصية تحمل دالة، name: (params) => ReturnType. يستخدمهما المستدعون بالطريقة نفسها.

الفرق دقيق: تحت strictFunctionTypes (جزء من strict) تُفحص معاملات الخصائص ذات نوع الدالة بصرامة، بينما تُفحص method signatures بتساهل أكبر (bivariantly)، لذلك تكتشف صيغة الخاصية أخطاء أكثر قليلًا. صياغة method أقصر وهي الأسلوب الأكثر شيوعًا؛ وكلتاهما مقبولة.

يمكن للواجهة أيضًا أن تصف شيئًا قابلًا للاستدعاء أو للإنشاء، باستخدام call signature أو construct signature:

interface Formatter {
  (value: number): string; // call signature: the object is a function
  locale: string;          // and it also has a property
}

interface PointConstructor {
  new (x: number, y: number): { x: number; y: number }; // construct signature
}

Index Signatures

عندما لا تكون أسماء الخصائص معروفة مسبقًا، يصفها index signature كلها دفعة واحدة: [key: string]: T تعني «أي مفتاح نصي، يحمل كل منها T».

تُظهر الأسطر الأخيرة المشكلة: قراءة مفتاح غير موجود نوعها number، لا number | undefined. وخيار المترجم noUncheckedIndexedAccess يضيف | undefined إلى كل قراءة من هذا النوع.

يمكن أن تقف خصائص مسماة بجانب index signature، لكن يجب أن تناسبه. interface Dict { [key: string]: number; name: string } هو الخطأ TS2411، Property 'name' of type 'string' is not assignable to 'string' index type 'number'. وسّع نوع الفهرس ([key: string]: number | string) أو انقل الجزء الديناميكي إلى خاصيته المستقلة. وللخرائط البسيطة من المفاتيح إلى القيم، يقول Record<string, number> الشيء نفسه في سطر واحد.

توسيع interface

يبني extends واجهة جديدة من واجهة موجودة أو أكثر. تملك الواجهة الابن كل أعضاء الأب إضافة إلى أعضائها:

interface Animal {
  name: string;
}

interface Pet extends Animal {
  owner: string;
}

interface Trained {
  commands: string[];
}

interface ServiceDog extends Pet, Trained {
  certifiedUntil: Date;
}

// ServiceDog requires: name, owner, commands, certifiedUntil

يمكن للابن إعادة تعريف خاصية من الأب بنوع متوافق (أضيق) فقط، مثل kind: "dog" حيث يقول الأب kind: string. القواعد، وطريقة توسيع type aliases، موجودة في صفحة extends.

تطبيق interface في صنف

يطلب class X implements Shape من المترجم التحقق من أن الصنف يملك كل ما تتطلبه الواجهة. العضو الغائب خطأ عند تصريح الصنف:

index.ts(7,7): error TS2420: Class 'Circle' incorrectly implements interface 'Shape'.
  Property 'area' is missing in type 'Circle' but required in type 'Shape'.

بعد إضافة area() يمكن استخدام عدة أصناف، بل وكائن عادي، كـ Shape:

implements فحص فقط. لا يضيف أعضاء إلى الصنف، ولا يحدد أنواع معاملات دوال الصنف نيابة عنك: greet(name) {} داخل صنف يطبّق greet(name: string): string ما زال الخطأ TS7006، Parameter 'name' implicitly has an 'any' type. ويمكن للصنف تطبيق عدة واجهات: class A implements B, C.

دمج التصريحات

تعريف واجهة بالاسم نفسه مرتين في النطاق نفسه يدمجهما في واجهة واحدة. هذا شيء لا تستطيعه type aliases (تعريف type ثانٍ بالاسم نفسه خطأ معرّف مكرر).

interface Settings {
  theme: string;
}

interface Settings {
  fontSize: number;
}

// Settings now requires both properties
const s: Settings = { theme: "dark", fontSize: 14 };

في كود التطبيقات نادرًا ما يكون هذا ما تريده، والدمج غير المقصود قد يسبب الارتباك. استخدامه الحقيقي هو إضافة أعضاء إلى أنواع لا تملكها: خيارات مكتبة، أو كائن عام مثل Window. ومن داخل وحدة، ضع التصريح داخل declare global:

declare global {
  interface Window {
    analytics: { track(event: string): void };
  }
}

export {};

بعد ذلك يجتاز window.analytics.track("signup") فحص الأنواع في كل مكان من المشروع. وتعتمد حزم تعريفات الأنواع على الآلية نفسها؛ انظر ملفات التعريف.

القيم الافتراضية لخصائص interface

لا يمكن للواجهة أن تحمل قيمًا افتراضية، لأنها تصف أنواعًا وتُمحى وقت التشغيل. size?: "sm" | "md" = "md" هو الخطأ TS1246، An interface property cannot have an initializer. اجعل الخاصية اختيارية واملأ القيمة الافتراضية حيث يُستخدم الكائن:

قيم التفكيك الافتراضية هي الخيار الأكثر أمانًا: تُطبَّق كلما كانت القيمة undefined، بما في ذلك size: undefined الصريحة. أما نسخة النشر فتنسخ تلك undefined الصريحة فوق القيمة الافتراضية، ويبقى نوع ناتجها كأن size مضبوطة دائمًا. إذا احتجت إلى هذا الضمان من الأنواع، ففعّل exactOptionalPropertyTypes، الذي يجعل size: undefined خطأ ترجمة لخاصية اختيارية size?: .... والصنف ذو الحقول المهيأة هو الخيار الآخر عندما يحتاج الكائن إلى سلوك أيضًا.

الواجهات العامة (Generic Interfaces)

يمكن للواجهة أن تأخذ معاملات أنواع، ما يجعل تعريفًا واحدًا يعمل مع أنواع حمولة كثيرة:

interface ApiResponse<T> {
  ok: boolean;
  data: T;
  error?: string;
}

interface Page<T> {
  items: T[];
  nextCursor?: string;
}

interface User {
  id: number;
  name: string;
}

const res: ApiResponse<Page<User>> = {
  ok: true,
  data: { items: [{ id: 1, name: "Ada" }], nextCursor: "abc" },
};

يُقرأ ApiResponse<Page<User>> على أنه «استجابة بياناتها صفحة من المستخدمين». والمكتبة المعيارية مليئة بها: Array<T> وPromise<T> وMap<K, V> كلها واجهات عامة.

interface مقابل type alias

يمكن لـ type alias أن يصف شكل الكائن نفسه، وفي أنواع الكائنات البسيطة يمكن استبدال أحدهما بالآخر. الواجهة وحدها تستطيع الدمج؛ وtype alias وحده يستطيع تسمية union أو tuple أو نوع mapped أو شرطي. قاعدة دليل TypeScript العملية هي استخدام interface حتى تحتاج إلى ميزة لا يملكها إلا type. وتحتوي صفحة interface مقابل type على المقارنة الكاملة، ومنها الفرق المتعلق بـ Record<string, ...> الذي يفاجئ معظم الناس.

الأسئلة الشائعة

ما هي interface في TypeScript؟

الـ interface وصف مسمّى لشكل الكائن: أسماء خصائصه وأنواعها، وأيها اختياري أو readonly، ودواله. يتحقق المترجم من أن القيم المستخدمة كتلك الواجهة لها ذلك الشكل. الواجهات موجودة وقت الترجمة فقط؛ ولا تنتج أي كود JavaScript.

كيف أضبط قيمة افتراضية في interface في TypeScript؟

لا يمكنك ذلك: الواجهة تصف أنواعًا لا قيمًا، لذلك size: "md" = ... ليست صياغة صحيحة. اجعل الخاصية اختيارية (size?: "sm" | "md") وطبّق القيمة الافتراضية حيث يُستخدم الكائن، عادة بقيم التفكيك الافتراضية في معاملات الدالة: function render({ size = "md" }: Options). ونشر كائن القيم الافتراضية ({ ...DEFAULTS, ...options }) يعمل أيضًا، لكن undefined الصريحة في options تستبدل القيمة الافتراضية.

كيف أتحقق وقت التشغيل من أن كائنًا يطبّق interface؟

لا توجد طريقة مدمجة، لأن الواجهات تُمحى أثناء الترجمة: obj instanceof User هو الخطأ TS2693 ('User' only refers to a type, but is being used as a value here). اكتب دالة type guard تفحص الخصائص، function isUser(x: unknown): x is User { ... }، أو تحقق باستخدام مكتبة مخططات.

هل يمكن لواجهة أن توسّع عدة واجهات؟

نعم. اسردها بعد extends مفصولة بفواصل: interface ServiceDog extends Pet, Trained { ... }. تملك الواجهة الجديدة كل أعضاء كل واجهة أب إضافة إلى أعضائها. وإذا أعلنت واجهتان أبوان الخاصية نفسها بأنواع غير متوافقة، يكون التصريح خطأ.

ما الفرق بين interface و class في TypeScript؟

الصنف (class) موجود وقت التشغيل: له مُنشئ وتطبيقات للدوال، وnew ينشئ منه كائنات. أما الواجهة فتصف شكلًا للمترجم فقط وتُمحى من ناتج JavaScript. يمكن للصنف أن يعلن implements SomeInterface ليتحقق المترجم من أنه يطابقها، وأي كائن عادي بالشكل الصحيح يناسب الواجهة أيضًا.

Coddy programming languages illustration

تعلّم البرمجة مع Coddy

ابدأ الآن