الواجهات في TypeScript

من موسوعة حسوب

مقدّمة

تركيز التحقق من الأنواع (type-checking) على شكل (shape) القيم من مبادئ TypeScript الأساسية. يُصطلَح عليه أحيانًا بالتعبير duck typing أو التحقق من الأنواع الفرعيّة هيكليًّا (structural subtyping). تعمل الواجهات في TypeScript على تسمية هذه الأنواع، وهي طريقة قويّة لتعريف عقود (contracts) داخل شيفرتك أو عقود مع شيفرةٍ خارج مشروعك.

واجهة بسيطة

لنبدأ بمثال بسيط لنفهم كيفيّة عمل الواجهات:

function printLabel(labelledObj: { label: string }) {
    console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

سيتحقّق مدقّق الأنواع (type-checker) من استدعاء الدالة ‎printLabel‎. تمتلك الدالة ‎printLabel‎ معاملًا واحدًا يتطلب (require) أن يكون الكائن المرّر محتويًا على خاصيّة باسم ‎label‎ نوعها سلسلة نصيّة ‎string‎. لاحظ أن الكائن الذي مرّرناه إلى الدالة عند استدعائها يملك خاصيّات أكثر من المطلوب، لكن المُترجم (compiler) يتحقّق من أنّ الخاصيات المطلوبة موجودة على الأقل وأنها توافق النوع المطلوب. هناك بعض الحالات التي لا تسمح فيها TypeScript بأن يكون الكائن مختلفًا قليلًا عمّا هو مطلوب (مثل زيادة خاصيّة)، وسنغطي ذلك بعد قليل.

يُمكننا كتابة نفس المثال مجدّدا، لكن هذه المرة سنستخدم واجهةً لوصف متطلب امتلاك خاصيّة باسم ‎label‎ نوعها سلسلة نصيّة:

interface LabelledValue {
    label: string;
}

function printLabel(labelledObj: LabelledValue) {
    console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

يُمكننا الآن استعمال اسم الواجهة ‎LabelledValue‎ لوصف المتطلب في المثال السابق. ولا يزال هذا المتطلب يُمثّل امتلاك خاصيّة باسم ‎label‎ نوعها سلسلة نصيّة. لاحظ أننا لم نحتج إلى تحديد أن الكائن الذي سيُمرّر إلى printLabel يُطبّق (implements) هذه الواجهة كما قد نحتاج إلى ذلك في لغات أخرى. والمهم هنا هو شكل الكائن فقط. إن كان الكائن الذي يُمرّر إلى الدالة متوافقًا مع المتطلبات المحدّدة فسيُسمَح بذلك.

لاحظ كذلك أن التحقق من الأنواع لا يتطلب أن تكون هذه الخاصيات مرتّبة بأي ترتيب محدّد، كل ما يتطلبه هو أن تكون الخاصيات المطلوبة في الواجهة موجودة في الكائن وأنها من النوع المطلوب.

الخاصيات الاختيارية

يُمكن لبعض الخاصيات في الواجهة ألا تكون مطلوبة. بحيث يكون بعضها موجودًا في حالات معيّنة ويُمكن ألا تكون موجودة بتاتًا. هذه الخاصيات الاختيارية شائعة عند إنشاء أنماط مثل "أكياس الخيارات (option bags)" التي يُمرّر فيها كائن إلى دالّة لديها قيم بعض الخاصيات مسبقًا.

إليك مثالًا لهذا النمط:

interface SquareConfig {
    color?: string;
    width?: number;
}

function createSquare(config: SquareConfig): {color: string; area: number} {
    let newSquare = {color: "white", area: 100};
    if (config.color) {
        newSquare.color = config.color;
    }
    if (config.width) {
        newSquare.area = config.width * config.width;
    }
    return newSquare;
}

let mySquare = createSquare({color: "black"});

تُكتَب الواجهات ذات خاصيات اختيارية كما تُكتَب الواجهات الأخرى، مع إلحاق المحرف ‎?‎ إلى نهاية اسم الخاصيّة الاختياريّة في التصريح.

ما يُميّز الخاصيات الاختياريّة هو أنك تستطيع وصف هذه الخاصيات التي يُمكن لها أن تكون موجودة في نفس الوقت الذي تمنع فيه استعمال خاصيات غير مُعرّفة في الواجهة. على سبيل المثال، لو أخطأنا في كتابة الخاصيّة ‎color‎ في ‎createSquare‎، لحصلنا على رسالة خطأ تُنبّهنا إلى أن اسم الخاصية خطأ:

interface SquareConfig {
    color?: string;
    width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
    let newSquare = {color: "white", area: 100};
    if (config.clor) {
        // Error: Property 'clor' does not exist on type 'SquareConfig'
        // خطأ: الخاصية
        // 'clor'
        // غير موجودة في النوع
        // 'SquareConfig'
        newSquare.color = config.clor;
    }
    if (config.width) {
        newSquare.area = config.width * config.width;
    }
    return newSquare;
}

let mySquare = createSquare({color: "black"});

الخاصيّات القابلة للقراءة فقط (Readonly properties)

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

interface Point {
    readonly x: number;
    readonly y: number;
}

يُمكنك بناء كائن من النوع ‎Point‎ عبر تعيين قيمة كائن حرفيّة (object literal). لا يُمكن تغيير قيمتي ‎x‎ و‎y‎ بعد التعيين.

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // خطأ، لا يُمكنك تعيين قيمة جديدة لخاصيةٍ قابلة للقراءة فقط

تُوفِّر TypeScript النوع ‎ ReadonlyArray<T>‎المُشابه للنوع ‎Array<T>‎ مع حذف جميع التوابع المُغيّرة (mutating methods)، لذا يُمكنك التيقّن من عدم تغيير المصفوفات بعد إنشائها:

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // خطأ
ro.push(5); // خطأ
ro.length = 100; // خطأ
a = ro; // خطأ

يُمكنك أن تُلاحظ في آخر سطر أعلاه أنّ إعادة تعيين كامل المصفوفة من النوع ‎ReadonlyArray‎ إلى مصفوفة عاديّة لا يجوز. لكنّك لا تزال تستطيع استبدالها باستعمال إثبات نوع (type assertion):

a = ro as number[];

readonly‎ مقابل ‎const

أسهل طريقة لتذكّر متى يجب استعمال ‎readonly‎ مقابل ‎const‎ هو أن تسأل ما إذا كنت تعمل مع متغيّر أو خاصيّة. المتغيرات تستعمل const والخاصيات تستعمل ‎readonly‎.

التحقق من الخاصيات الزائدة (Excess Property Checks)

في مثال الواجهات الأول أعلاه، سمحت لنا TypeScript بتمرير ‎{ size: number; label: string; }‎ إلى شيء لم يتوقّع إلا خاصية ذات المواصفات ‎{ label: string; }‎. وتعرّفنا قبل قليل على الخاصيات الاختيارية وكيف أنها مفيدة عند وصف ما يُطلق عليه بأكياس الخيارات.

لكن دمج كلا المبدأين دون تفكير سيؤدي إلى وجع رأس كما قد يحدث لو كنت تستخدم JavaScript. مثلًا، خذ مثال ‎createSquare‎ السابق:

interface SquareConfig {
    color?: string;
    width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
    // ...
}

let mySquare = createSquare({ colour: "red", width: 100 });

لاحظ أنّ المعامل المرر للدالة ‎createSquare‎ مكتوب على شكل ‎colour‎ عوضًا عن ‎color‎. ستمرّ علّة كهذه في JavaScript بصمت دون أي إنذار. قد تقول بأن هذا البرنامج صحيح لأن خاصيتا ‎width‎ متوافقتان، ولا يوجد خاصية باسم ‎color‎، والخاصية ‎colour‎ الزائدة لن تؤثّر.

لكنّ TypeScript تعتقد بأن هناك علّة في هذه الشيفرة. إذ أن قيم الكائنات الحرفيّة (object literals) تمرّ عبر تحقّق من الخاصيات الزائدة عند تعيينها لمتغيّرات أخرى، أو عند تمريرها كمُعاملات. إن كانت قيمة كائن حرفيّة تملك خاصيّة لا يملكها النوع الهدف (target type)، فستحصل على خطأ:

// خطأ، الخاصيّة
// 'colour'
// غير مُتوقَّعة في النوع
// 'SquareConfig'
let mySquare = createSquare({ colour: "red", width: 100 });

تخطّي التحقق من الخاصيات الزائدة هذا بسيط للغاية. أسهل طريقة لفعل ذلك هو باستخدام إثبات نوع (type assertion) كما يلي:

let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

لكن قد تكون هناك طريقة أفضل إذا كنت متأكّدًا من أن الكائن قد يملك خاصيّات زائدة ستُستعمل بشكل خاص، وذلك عبر إضافة توقيع فهرس سلسلة نصيّة (string index signature) إن أمكن للواجهة ‎SquareConfig‎ أن تملك الخاصيتان ‎color‎ و‎width‎ ذات الأنواع أعلاه، لكن قد تملك كذلك أي عدد من الخاصيات الأخرى، عندها يُمكننا تعريف الواجهة كما يلي:

interface SquareConfig {
    color?: string;
    width?: number;
    [propName: string]: any;
}

سنتحدث عن توقيعات الفهرس بعد قليل، المهم أنّنا نقوم هنا بتحديد أن ‎SquareConfig‎ قد تملك أي عدد من الخاصيات، وما دام أنها ليست ‎color‎ أو ‎width‎، فنوعها لا يهم.

أخيرًا، يُمكن تعيين الكائن إلى متغيّر آخر لتجاوز هذا التحقق، وهي طريقة قد تكون مفاجأة قليلًا، لأنّ ‎squareOptions‎ لن يمرّ بالتحقق من الخاصيات الزائدة، والمترجم (compiler) لن يطلق أي خطأ:

let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

تذكّر بأنه لا يتوجّب عليك ربما أن تتجاوز هذه التحقّقات في حالة كانت الشيفرة بسيطة كما في المثال أعلاه. لكن قد تحتاج إلى هذه التقنيات عند التعامل مع كائنات معقّدة تملك توابع وتحمل حالة (state)، لكن معظم أخطاء الخاصيات الزائدة تكون عللًا في حقيقة الأمر. هذا يعني بأنّك إذا حدث وأن حصلت على مثل هذه الأخطاء عند الاعتماد على أكياس الخيارات أو ما شابه، فقد تحتاج إلى مراجعة تصريحات الأنواع (type declarations) الخاصة بك. في هذه الحالة، إن لم يُسبّب تمرير كائن ذو خاصيّة باسم ‎color‎ أو ‎colour‎ إلى ‎createSquare‎ أية مشاكل، فعليك إصلاح تعريف ‎SquareConfig‎ لتُوضّح ذلك.

أنواع الدوال (Function Types)

يُمكن للواجهات أن تصف نطاقًا واسعًا من الأشكال التي قد تكون عليها كائنات JavaScript. وتصف الواجهات أنواع الدوال إضافة لوصف كائن وخاصياته.

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

interface SearchFunc {
    (source: string, subString: string): boolean;
}

بعد تعريف واجهة نوع الدالة (function type interface) هذه، يُمكننا استعمالها كما نستعمل أي واجهة أخرى. المثال التالي يُوضّح كيفيّة إنشاء متغيّر يحمل نوع دالة وتعيينه لقيمة دالة (function value) من نفس النوع:

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    let result = source.search(subString);
    return result > -1;
}

لا تحتاج أسماء المعاملات إلى أن تكون متطابقة ليُتحقّق من أنواع الدوال بشكل صحيح. كان يُمكننا مثلا أن نكتب المثال أعلاه بالشكل التالي:

let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
    let result = src.search(sub);
    return result > -1;
}

يُتحقَّقُ من معاملات الدوال واحدًا بواحد، ويُتحقّق من النوع في موقع المعامل مع نوع المعامل المقابل له. إن لم ترغب بتحديد الأنواع، يُمكن للتحقق من الأنواع السياقيّ (contextual typing) في TypeScript أن يستنتج أنواع المعاملات لأن قيمة الدالة قد عُيِّنت مباشرة لمتغيّر من النوع ‎SearchFunc‎. ونوع القيمة المعادة في تعبير الدالة هنا يُقرَّر من القيمة التي تُعيدها (القيمة ‎false‎ أو القيمة ‎true‎ في هذه الحالة). لو أعاد تعبير الدالة عددًا أو سلسلة نصيّة، لنبّهَنا مُدقّق الأنواع بأن القيمة المعادة لا توافق القيمة المعادة الموصوفة في الواجهة ‎SearchFunc‎.

let mySearch: SearchFunc;
mySearch = function(src, sub) {
    let result = src.search(sub);
    return result > -1;
}

الأنواع القابلة للفهرسة (Indexable Types)

يُمكننا بطريقة مُشابهة وصفُ الأنواع التي يُمكننا استعمال الفهرسة معها مثل ‎a[10]‎‎ أو‎ageMap["daniel"]‎ ‎. تملك الأنواع القابلة للفهرسة توقيع فهرس (index signature) يصف الأنواع التي يُمكننا استعمالها لفهرسة (index into) الكائن، إضافةً إلى الأنواع المعادة عند الفهرسة. لنلق نظرة على المثال التالي:

interface StringArray {
    [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

لدينا في المثال أعلاه واجهة باسم ‎StringArray‎ ذات توقيع فهرس. يُشير توقيع الفهرس هذا بأنّه إن استخدمت الفهرسة مع مصفوفة من النوع ‎StringArray‎ باستعمال عددٍ (‎number‎)، فستعيد سلسلةً نصيّة (‎string‎).

هناك نوعان من توقيعات الفهارس: السلاسل النصيّة والأعداد. من الممكن دعم كلا نوعي المُفهرسات (indexers)، لكن يجب على النوع المُعاد من مُفهرس عددي أن يكون نوعًا فرعيًّا من النوع المعاد من مُفهرس السلسلة النصيّة (string indexer). هذا لأنه عند الفهرسة بعدد (‎number‎)، فستُحوّله لغة JavaScript إلى سلسلة نصيّة (‎string‎) قبل فهرسة الكائن. هذا يعني بأن الفهرسة بالعدد ‎100‎ هي نفسها الفهرسة باستخدام السلسلة النصيّة ‎"100"‎‎، لذا يجب على كليهما أن يتناسقا:

class Animal {
    name: string;
}
class Dog extends Animal {
    breed: string;
}

// خطأ، الفهرسة بسلسلة نصيّة عددية قد يجلب نوعًا مختلفًا من الصنف
// Animal
interface NotOkay {
    [x: number]: Animal;
    [x: string]: Dog;
}

ورغم أن توقيعات الفهرس النصيّة (string index signatures) طريقة مُجدية لوصف نمط القواميس (dictionary)، إلا أنها تقضي بوجوب توافق جميع الخاصيات مع نوع القيمة المعادة الخاصة بها. هذا لأن الفهرس النصي (string index) يُصرّح بأنّ ‎‎obj.property‎‎ متاح كذلك على الشكل ‎‎obj‎["‎property‎"‎‎]‎‎.

في المثال التالي، نوع ‎name‎ لا يوافق نوع الفهرس النصي، لذا فمدقّق الأنواع يُطلق خطأ:

interface NumberDictionary {
    [index: string]: number;
    length: number;    // الطول عددٌ، وهذا مسموح به
// error, the type of 'name' is not a subtype of the indexer
// خطأ، نوع
// 'name'
// ليس نوعًا فرعيًّا للمُفهرس
    name: string;
}

أخيرًا، يُمكنك جعل توقيعات الفهرس قابلة للقراءة فقط (readonly) لمنع تعيين قيم لفهارسها:

interface ReadonlyStringArray {
    readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // خطأ

لا يُمكنك تعيين قيمة للعنصر ‎myArray[2]‎‎ لأن توقيع الفهرس قابل للقراءة فقط.

أنواع الأصناف (Class Types)

تطبيق واجهة (Implementing an interface)

اقتضاءُ وجوب توافق صنفٍ مع عقد (contract) معيّن من الاستعمالات الشائعة للواجهات في لغات مثل C#‎ وJava، وهذا ممكن كذلك في TypeScript.

interface ClockInterface {
    currentTime: Date;
}

class Clock implements ClockInterface {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

يُمكن كذلك وصف التوابع في الواجهة التي يُطبقها الصنف، كما هو موضح بالتابع ‎setTime‎ في المثال التالي:

interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}

class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}

تصف الواجهات الجانب العموميّ (public side) من الصنف عوضًا عن كلا الجانبين العمومي والخاص (private side). ما يمنع من استعمال الواجهات للتحقّق من أن صنفًا يملك كذلك نوعًا معيّنًا على الجانب الخاص من نسخة الصنف (the class instance).

الفرق بين الجانب الساكن (static) وجانب النّسخة (instance) للأصناف

عند العمل مع الأصناف والواجهات، من المُفيد تذكّر أنّ للصّنف نوعين مختلفين: نوع الجانب الساكن ونوع جانب النسخة. قد تُلاحظ بأنّك إن أنشأت واجهة بتوقيع بِناء (construct signature) وحاولت إنشاء صنف يُطبّق هذه الواجهة فستحصل على خطأ:

interface ClockConstructor {
    new (hour: number, minute: number);
}

class Clock implements ClockConstructor {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

هذا لأنّه عندما يُطبّق صنف واجهة معيّنة، فلا يُتحقَّقُ إلا من جانب النسخة. ولأن البنّاء (constructor) موجود على الجانب الساكن فلن يشمله هذا التحقّق.

ستحتاج عوضًا عن هذا إلى العمل مع الجانب الساكن من الصنف مباشرةً. في هذا المثال، نُعرّف واجهتان، الأولى ‎ClockConstructor‎ للبنّاء و‎ClockInterface‎ لتوابع النسخة (instance methods). بعدها نُعرّف دالّة بانيّة (constructor function) باسم ‎createClock‎ لتسهيل إنشاء نسخ من النوع المُمرّر إليها.

interface ClockConstructor {
    new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
    tick();
}

function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
    return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("beep beep");
    }
}
class AnalogClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("tick tock");
    }
}

let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

بسبب أنّ معامل الدالة ‎ createClock‎الأول من النوع ‎ClockConstructor‎، فسيُتحقَّق من أنّ ‎AnalogClock‎ يمتلك توقيع بنّاء صحيحًا عند الاستدعاء ‎‎createClock‎(AnalogClock‎, 7‎, 32‎)‎‎.

توسيع الواجهات

يُمكن للواجهات أن تُوسّع (extend) بعضها البعض مثل الأصناف. يسمح هذا بنسخ عناصر واجهة إلى أخرى، ما يُعطي مرونة أكثر في كيفيّة تقسيم واجهاتك إلى مكونات يُمكن إعادة استعمالها.

interface Shape {
    color: string;
}

interface Square extends Shape {
    sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;

يُمكن لواجهة أن تُوسّع عدّة واجهات، مُنشِأةً مزيجًا من جميع الواجهات التي توسّعها:

interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

interface Square extends Shape, PenStroke {
    sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

الأنواع الهجينة (Hybrid Types)

يُمكن للواجهات -كما سبق ذكره- أن تصف الأنواع الغنيّة الموجودة في لغة JavaScript على أرض الواقع. وبسبب طبيعة JavaScript الديناميكيّة والمرنة، فقد تجد أحيانًا كائنًا يعمل كمزيج من بعض الأنواع الموصوفة أعلاه.

الكائنات التي تعمل مثل دالة وكائن في نفس الوقت من الأمثلة على ذلك، إضافةً إلى خاصيات إضافيّة.

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}

function getCounter(): Counter {
    let counter = <Counter>function (start: number) { };
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

قد تحتاج إلى استعمال أنماط مثل النمط أعلاه عند استعمال شيفرة JavaScript من طرف ثالث (3rd-party)لوصف شكل النوع كاملًا.

توسيع الأصناف بالواجهات

عندما يُوسّع نوعُ واجهةٍ (interface type) نوعَ صنفٍ (class type)، فسيرث (inherit) عناصر الصنف لكنّه لن يرث الواجهات التي يُطبّقها الصنف. وكأنّ الواجهة تُعرّف جميع عناصر الصنف دون توفير أي تطبيق (implementation). ترث الواجهات حتى عناصر الصنف الأساس التي تكون خاصّة (private) أو محميّة (protected). ما يعني أنه عند إنشاء واجهة تُوسّع صنفا ذا عناصر خاصّة أو محميّة، فنوع الواجهة قابل لتطبيقه فقط من هذا الصنف أو صنف فرعي له.

هذا مُفيد إن أردت إنشاء شجرة وراثة (inheritance hierarchy) كبيرة وأدرت أن تعمل الشيفرة فقط مع الأصناف الفرعيّة ذات خاصيّات مُعيّنة. ولا تحتاج الأصناف الفرعيّة إلى أن تكون مرتبطةً مع بعضها، المهم فقط أن ترث من الصنف الأساس. مثلًا:

class Control {
    private state: any;
}

interface SelectableControl extends Control {
    select(): void;
}

class Button extends Control implements SelectableControl {
    select() { }
}

class TextBox extends Control {
    select() { }
}

// خطأ، الخاصيّة
// 'state'
// مفقودة في النوع
// 'Image'
class Image implements SelectableControl {
    select() { }
}

class Location {

}

في المثال أعلاه، يحتوي ‎SelectableControl‎ على جميع عناصر ‎Control‎، بما فيها الخاصيّة الخاصّة ‎state‎. ولأنّ ‎state‎ عنصر خاصّ فمن الممكن فقط لأولاد (descendants) الصنف ‎Control‎ أن يُطبّقوا الواجهة ‎SelectableControl‎. وهذا لأن أولاد ‎Control‎ هم فقط من سيملك عنصر ‎state‎ خاصًّا نابعًا من نفس التصريح، والذي يُعدّ متطلبًا لتكون العناصر الخاصّة متوافقة.

من الممكن الوصول إلى العنصر الخاصّ ‎state‎ داخل الصنف ‎Control‎ عبر نسخة من ‎SelectableControl‎. عمليًّا، يتصرّف ‎SelectableControl‎ مثل صنف ‎Control‎ معروفٍ بأنّه يملك تابعًا باسم ‎select‎. الصّنف ‎Button‎ والصّنف ‎TextBox‎ نوعان فرعيّان من ‎SelectableControl‎ (لأنّ كلاهما يرث من ‎Control‎ ويملك تابعًا باسم ‎select‎)، لكنّ الصنفين ‎Image‎ و‎Location‎ ليسا نوعين فرعيّين منه.

مصادر