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

من موسوعة حسوب
اذهب إلى التنقل اذهب إلى البحث

مقدمة

تصِف بعض المبادئ الفريدة الموجودة في لغة TypeScript شكل كائنات JavaScript على مستوى الأنواع. مبدأ دمج التصريحات (declaration merging) من المزايا التي تنفرد بها TypeScript. سيُساعد فهم هذا المبدأ على العمل مع شيفرة JavaScript الموجودة مسبقًا. إضافة إلى أنّها توفّر قدرة على إعمال مبادئ تجريد (abstraction concepts) أكثر تقدّمًا.

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

المبادئ الأساسيّة

تُنشئ التصريحات في TypeScript الكائنات من مجموعة واحدة على الأقل من أصل ثلاثة مجموعات: مجال أسماء (namespace)، أو نوع (type)، أو قيمة (value). تُنشئ تصريحات مجالات الأسماء مجال أسماء يحتوي على أسماء يُمكن الوصول إليها عن طريق النقاط (‎.‎ مثل ‎User.Login‎). تُنشئ تصريحات الأنواع نوعًا مرئيًّا ذو الشكل المُصرَّح عنه ومرتبطٍ بالاسم المعطى له. أمّا تصريحات القيم فتنشئ قيمًا مرئيّةً في شيفرة JavaScript المُخرَجَة.

نوع التصريح مجال أسماء نوع قيمة
مجال أسماء X X
صنف X X
ثابت متعدّد X X
واجهة X
تسمية نوع بديلة X
دالة X
متغير X

سيساعدك فهم ما يُنشأ عند كل تصريح على فهم ما يُدمج عندما تؤدي عمليّة دمج التصريحات.

دمج الواجهات

دمج الواجهات أبسط وأشهر نوع من أنواع دمج التصريحات. يقوم الدمج على أبسط نحو بجمع عناصر التصريحين في واجهة واحدة بنفس الاسم.

interface Box {
    height: number;
    width: number;
}

interface Box {
    scale: number;
}

let box: Box = {height: 5, width: 6, scale: 10};

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

أما العناصر التي تكون دالة فسيُعامَل كل عنصر دالة من نفس الاسم على أنّه يصف حملًا زائدًا (overload) لنفس الدالة. لاحظ كذلك أنّه عند دمج واجهةٍ ‎A‎ مع واجهة أخرى تسمى كذلك ‎A‎، فسيكون للواجهة الثانيّة أسبقيّة أعلى من الأولى (أي أن عناصر الواجهة الثانية ستسبق عناصر الواجهة الأولى في الواجهة المندمجة الناتجة).

هذا المثال يوضح الفكرة:

interface Cloner {
    clone(animal: Animal): Animal;
}

interface Cloner {
    clone(animal: Sheep): Sheep;
}

interface Cloner {
    clone(animal: Dog): Dog;
    clone(animal: Cat): Cat;
}

ستُدمَج الواجهات الثلاثة لإنشاء تصريح واحد:

interface Cloner {
    clone(animal: Dog): Dog;
    clone(animal: Cat): Cat;
    clone(animal: Sheep): Sheep;
    clone(animal: Animal): Animal;
}

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

التوقيعات المخصّصة (specialized signatures) استثناءٌ لهذه القاعدة. إذا كان لتوقيعٍ معاملٌ نوعُه نوعُ سلسلة نصيّة حرفيّة (string literal type) واحد، عوضًا عن اتحاد أنواع سلاسل نصيّة حرفية، فعندئذ سيُنقل إلى أعلى قائمة الأحمال الزائدة المدمجة.

مثلًا، ستُدمَج الواجهات التالية:

interface Document {
    createElement(tagName: any): Element;
}
interface Document {
    createElement(tagName: "div"): HTMLDivElement;
    createElement(tagName: "span"): HTMLSpanElement;
}
interface Document {
    createElement(tagName: string): HTMLElement;
    createElement(tagName: "canvas"): HTMLCanvasElement;
}

وستكون نتيجة دمج تصريحات ‎Document‎ كالتالي:

interface Document {
    createElement(tagName: "canvas"): HTMLCanvasElement;
    createElement(tagName: "div"): HTMLDivElement;
    createElement(tagName: "span"): HTMLSpanElement;
    createElement(tagName: string): HTMLElement;
    createElement(tagName: any): Element;
}

دمج مجالات الأسماء

تُدمَج عناصر مجالات الأسماء ذات نفس الاسم كذلك كما هي الحال مع الواجهات. ولأنّ مجالات الأسماء تُنشئ كلًّا من مجال أسماء وقيمة في نفس الوقت، فنحتاج أولًا إلى فهم آلية اندماجهما.

لدمج مجالات الأسماء، تُدمَج تعريفات الأنواع من الواجهات المُصدَّرة التي يُصرَّح عنها في كل مجال أسماء، وينتج عن ذلك مجال أسماء واحد تعريفات واجهاته مُندمجة داخله.

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

دمج التصريح عن ‎Animals‎ في هذا المثال:

namespace Animals {
    export class Zebra { }
}

namespace Animals {
    export interface Legged { numberOfLegs: number; }
    export class Dog { }
}

مكافئ لما يلي:

namespace Animals {
    export interface Legged { numberOfLegs: number; }

    export class Zebra { }
    export class Dog { }
}

نموذج دمج مجالات الأسماء هذا مفيدٌ كبداية، لكننا نحتاج كذلك إلى فهم ما يحدث للعناصر التي لم تُصدَّر بالكلمة المفتاحية ‎export‎. تكون العناصر غير المصدّرة مرئيّةً فقط في مجال الأسماء الأصلي الذي لم يُدمَج. هذا يعني أن العناصر التي دُمجَت مع عناصر التصريحات الأخرى لا يُمكنها الوصول إلى العناصر التي لم تُصدّر في مجالات الأسماء الأخرى (ولو بعد دمجها معًا).

بالمثال يتّضح المقال:

namespace Animal {
    let haveMuscles = true;

    export function animalsHaveMuscles() {
        return haveMuscles; // يمكننا الوصول إلى المتغير لأننا في نفس مجال الأسماء الذي عُرِّف فيه
    }
}

namespace Animal {
    export function doAnimalsHaveMuscles() {
       // خطأ، لا يمكن الوصول هنا إلى العنصر
       // haveMuscles
       // لأنه لم يُصدَّر
        return haveMuscles;
    }
}

الدالة ‎animalsHaveMuscles‎ وحدها فقط من يُمكنها الوصول إلى الرمز ‎haveMuscles‎ لأنّهما يشتركان في نفس مجال الأسماء قبل الدمج. أمّا الدالة ‎doAnimalsHaveMuscles‎ فلا يُمكنها رؤية هذا العنصر غير المُصدَّر ولو كانت جزءًا من مجال الأسماء ‎Animal‎ المندمج.

دمج مجالات الأسماء مع الأصناف، والدوال، والثوابت المتعددة

تُدمج مجالات الأسماء بسبب مرونتها مع أنواع التصريحات الأخرى كذلك. وللقيام بهذه العمليّة، يجب على التصريح عن مجال الأسماء أن يتبع التصريح الذي سيُدمَج معه. سيملك التصريح الناتج خاصيات كل من نوعيّ التصريحات. تستخدم TypeScript هذه القدرة لتمثيل بعض أنماط JavaScript وبعض لغات البرمجة الأخرى.

دمج أسماء المجالات مع الأصناف

يعطي هذا للمستخدم طريقة لوصف الأصناف الداخليّة:

class Album {
    label: Album.AlbumLabel;
}
namespace Album {
    export class AlbumLabel { }
}

تَتبّع قواعد الظهور (التي تحدد متى تكون العناصر التي دُمجَت مرئيّةً أو غير مرئيّة) نفس القواعد المشروحة في قسم دمج مجالات الأسماء أعلاه، لذا من الواجب تصدير الصّنف ‎AlbumLabel‎ ليتمكّن الصنف المُدمَج من الوصول إليه. النتيجة النهائية هي صنفٌ موجود داخل صنف آخر. يُمكنك كذلك استخدام مجالات الأسماء لإضافة المزيد من العناصر الساكنة لصنف موجود مسبقًا.

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

function buildLabel(name: string): string {
    return buildLabel.prefix + name + buildLabel.suffix;
}

namespace buildLabel {
    export let suffix = "";
    export let prefix = "Hello, ";
}

console.log(buildLabel("Sam Smith"));

وبالمثل يُمكن استخدام مجالات الأسماء لتوسيع الثوابت المتعددة ذات عناصر ساكنة:

enum Color {
    red = 1,
    green = 2,
    blue = 4
}

namespace Color {
    export function mixColor(colorName: string) {
        if (colorName == "yellow") {
            return Color.red + Color.green;
        }
        else if (colorName == "white") {
            return Color.red + Color.green + Color.blue;
        }
        else if (colorName == "magenta") {
            return Color.red + Color.blue;
        }
        else if (colorName == "cyan") {
            return Color.green + Color.blue;
        }
    }
}

أنواع الدمج المرفوضة

لا يُسمح بجميع أنواع الدمج في TypeScript. إذ لا يمكن في الوقت الحالي دمج الأصناف مع أصناف أخرى أو مع المتغيرات. انظر توثيق المخاليط في TypeScript للمزيد من المعلومات حول محاكاة دمج الأصناف.

تنمية الوحدات (Module Augmentation)

رغم أنّ وحدات JavaScript لا تدعم الدمج، إلا أنّك تستطيع ترقيع الكائنات الموجودة عبر استيرادها ثمّ تحديثها. لنلق نظرة على مثال مزيّف لنمط Observable:

// observable.js
export class Observable<T> {
    // ... التفاصيل لا تهم هنا ...
}

// map.js
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
    // ... تفاصيل آلية العمل غير مهمّة في هذا المثال
}

هذا يعمل في TypeScript كذلك، لكن المترجم لا يملك أية معلومات عن ‎Observable.prototype.map‎. يُمكنك الاعتماد على تنمية الوحدات لإعلام المترجم عنها:

// يبقى الملفّ
// observable.ts
// كما هو

// map.ts
import { Observable } from "./observable";
declare module "./observable" {
    interface Observable<T> {
        map<U>(f: (x: T) => U): Observable<U>;
    }
}
Observable.prototype.map = function (f) {
    // ... تفاصيل آلية العمل غير مهمّة في هذا المثال
}


// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable<number>;
o.map(x => x.toFixed());

يُقرَّر اسم الوحدة بنفس طريقة مُحدِّدات الوحدات في الجملة ‎import‎ والجملة ‎export‎. انظر توثيق الوحدات للاستزادة. تُدمج التصريحات في تنمية الوحدة وكأنها صُرِّحت في نفس الملف الأصلي. لكن لا يُمكنك التصريح عن تصريحات جديدة في المستوى الأعلى (top-level) في التنمية (يُسمَح فقط بترقيع التصريحات الموجودة أصلًا).

التنمية العامّة (Global augmentation)

يُمكن كذلك إضافة التصريحات إلى المجال العام من داخل وحدة معيّنة:

// observable.ts
export class Observable<T> {
    // ...
}

declare global {
    interface Array<T> {
        toObservable(): Observable<T>;
    }
}

Array.prototype.toObservable = function () {
    // ...
}

تسلك التنميات العامّة نفس سلوك الوحدات وعليها نفس القيود كذلك.

مصادر