المزخرفات في TypeScript

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

مقدمة

مع إضافة الأصناف إلى TypeScript ونسخة ES6، فقد ظهرت كذلك حاجةٌ إلى ميزات إضافية لدعم تعديل الأصناف وعناصر الأصناف أو توصيفها (annotating). توفّر المزخرفات (Decorators) طريقة لإضافة توصيفات وبنية برمجة وصفيّة (meta-programming) لتصريحات الأصناف وعناصرها. المزخرفات حاليًّا اقتراح في المرحلة 2 (stage 2 proposal) في JavaScript وهي متوفّرة كميّزة تجريبيّة في TypeScript.

ملاحظة: المزخرفات ميّزة تجريبيّة قد تتغيّر في النسخ الجديدة مستقبلًا.

لتفعيل الدعم التجريبي للمزخرفات، عليك تفعيل خيار المترجم ‎experimentalDecorators‎ إما على سطر الأوامر أو في ملفّ ‎tsconfig.json‎ الخاصّ بك:

سطر الأوامر:

tsc --target ES5 --experimentalDecorators

tsconfig.json‎:

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}

المزخرفات

المزخرف نوع خاص من أنواع التصريحات يُمكن وضعه على تصريح صنفٍ، أو تابع (method)، أو تابع وصول (accessor)، أو خاصيّة (property)، أو معامل (parameter). تستخدم المزخرفات الشكل ‎@expression‎، بحيث يجب على ‎expression‎ أن يُقدَّر إلى (evaluate to) دالةٍ يُمكن استدعاؤها أثناء التنفيذ (runtime) مع تمرير معلومات عن التصريح المُزخرَف.

على سبيل المثال، لنفترض أن لدينا مزخرفًا باسم ‎@sealed‎، يُمكن أن نكتب الدالة ‎sealed‎ كما يلي:

function sealed(target) {
    // إجراء عمليات على الكائن
    // target
}

ملاحظة: يُمكنك إلقاء نظرة على مثال مزخرف أدقّ في قسم مزخرفات الأصناف أدناه.

مولِّدات المزخرفات (Decorator Factories)

يُمكننا استخدام مولِّد مزخرفات إن أردنا تخصيص كيفيّة تطبيق مزخرفٍ لتصريح ما. مولِّد المزخرفات هو ببساطةٍ دالّةٌ تُعيد التعبير الذي سيُستدعَى من طرف المزخرف أثناء التنفيذ.

يُمكننا كتابة مولِّد مزخرفات كما يلي:

function color(value: string) { // هذا هو مولّد المزخرفات
    return function (target) { // هذا هو المزخرف
        // يُمكن الآن إجراء عمليّات على
        // 'target' و 'value'
    }
}

ملاحظة: يُمكنك إلقاء نظرة على مثال أدقّ لمولِّد مزخرفاتٍ في قسم مزخرفات التوابع أدناه.

تركيب المزخرفات (Decorator Composition)

يُمكن تطبيق أكثر من مزخرف واحد على تصريح ما كما في المثالين التاليين:

  • في سطر واحد:
@f @g x
  • في عدّة أسطر:
@f
@g
x

عندما تُطبَّق عدّة مزخرفات لتصريح واحد، فتقديرها يكون مماثلًا مع تركيب الدوال في الرياضيات. عند تركيب دالتين ‎f‎ و‎g‎ في هذا النموذج، فالمركَّب الناتج ‎‎(f ∘ g)‎(x)‎‎ مكافئ للاستدعاء ‎‎f(g(x))‎‎.

تُتَّبع الخطوات التالية عند تقدير عدّة مزخرفات على تصريح واحد في TypeScript:

  1. تُقدَّر تعابير المزخرفات من الأعلى إلى الأسفل.
  2. تُستدعى النتائج بصفتها دوالًا من الأسفل إلى الأعلى.

يُمكننا ملاحظة ترتيب التقدير هذا باستخدام مولِّدات المزخرفات في المثال التالي:

function f() {
    console.log("f(): تقدير");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("f(): استدعاء");
    }
}

function g() {
    console.log("g(): تقدير");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("g(): استدعاء");
    }
}

class C {
    @f()
    @g()
    method() {}
}

ما سيطبع المخرج التالي على الطرفية:

f(): تقدير  
g(): تقدير  
g(): استدعاء  
f(): استدعاء

تقدير المزخرفات (Decorator Evaluation)

تُطبَّق المزخرفات بترتيب محدّد على مختلف أنواع التصريحات داخل الصنف:

  1. تُطبَّق مزخرفات المعاملات، ثمّ مزخرفات التوابع، ثم مزخرفات دوال الوصول، ثم بعد ذلك مزخرفات الخاصيات لكل عنصر من عناصر النسخة (instance).
  2. تُطبَّق مزخرفات المعاملات، ثمّ مزخرفات التوابع، ثم مزخرفات دوال الوصول، ثم بعد ذلك مزخرفات الخاصيات لكل عنصر من العناصر الساكنة (static).
  3. تُطبَّق مزخرفات المعاملات على الدالة البانية (constructor).
  4. تطبق مزخرفات الأصناف على الصنف.

مزخرفات الأصناف

يُصرَّح عن مزخرف صنف مباشرة قبل التصريح عن الصنف. يُطبَّق مزخرف الصنف على الدالة البانيّة للصنف ويُمكن استخدامه لملاحظة تعريف الصنف أو تعديله أو استبداله بآخر. لا يمكن استخدام مزخرف صنف في ملف تصريحات (declaration file)، أو في أي سياق محيط (ambient context) آخر (مثل استخدام المزخرف على صنفٍ مصرَّح عنه بالكلمة المفتاحية ‎declare‎).

سيُستدعى تعبير مزخرف الصنف كدالة أثناء التنفيذ مع تمرير الدالة البانية الخاصة بالصنف المزخرَف كمُعامل وحيد.

إذا أعاد مزخرف الصنف قيمة معينة، فستستبدل تصريح الصنف بالدالة البانية المعطاة.

ملاحظة: إذا اخترت إعادة دالة بانية، عليك الإبقاء على سلسلة prototype الأصليّة. المنطق (logic) الذي يطبق المزخرفات أثناء التنفيذ لن يؤدي هذه المهمة عنك.

ما يلي مثال على مزخرف صنفٍ (‎@sealed‎) يُطبَّق على الصنف ‎Greeter‎:

@sealed
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

يُمكننا تعريف المزخرف ‎@sealed‎ باستخدام تصريح الدالة التالي:

function sealed(constructor: Function) {
    Object.seal(constructor);
    Object.seal(constructor.prototype);
}

عند تنفيذ المزخرف ‎@sealed‎، سيُختَم كل من الدالة وسلسلة prototype الخاصة بها (انظر توثيق ‎Object.seal‎). تاليًا، إليك مثالًا لكيفيّة استبدال الدالة البانيّة:

function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
    return class extends constructor {
        newProperty = "new property";
        hello = "override";
    }
}

@classDecorator
class Greeter {
    property = "property";
    hello: string;
    constructor(m: string) {
        this.hello = m;
    }
}

console.log(new Greeter("world"));

مزخرفات التوابع (Method Decorators)

يُصرَّح عن مزخرفات التوابع مباشرة قبل التصريح عن التابع المرغوب زخرفته. يُطبَّق المزخرف على واصف الخاصية (Property Descriptor) الخاص بالتابع (انظر توثيق الدالة ‎Object.defineProperty()‎‎‎ للاستزادة)، ويُمكن استخدامه لملاحظة تعريف التابع، أو تعديله، أو استبداله. لا يمكن استخدام مزخرف تابع في ملف تصريحات (declaration file)، أو على حمل زائد (overload)، أو في أي سياق محيط (ambient context) آخر (مثل استخدامه على صنفٍ مصرَّح عنه بالكلمة المفتاحية ‎declare‎). سيُستدعى تعبير مزخرف التابع كدالة أثناء التنفيذ مع تمرير المعاملات الثلاثة التالية:

  1. إمّا الدالة البانية (constructor function) الخاصّة بالصنف لعنصرٍ ساكن (static member)، أو سلسلة prototype الخاصة بالصنف لعنصر نسخةٍ (instance member).
  2. اسم العنصر (أي اسم التابع في هذه الحالة).
  3. واصف الخاصيّة للعنصر.

ملاحظة: قيمة واصف الخاصية ستكون ‎undefined‎ إذا كنت تستهدف نسخًا أقدم من ‎ES5‎.

إذا أعاد مزخرف التابع قيمة ما، فستُستخدم هذه القيمة كواصف خاصيّة للتابع.

ملاحظة: تُتَجاهل القيمة التي يُعيدها مزخرف التابع إذا كنت تستهدف نسخًا أقدم من ‎ES5‎.

ما يلي مثال على مزخرف تابع (‎@enumerable‎) يُطبَّق على تابعٍ في الصنف ‎Greeter‎:

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }

    @enumerable(false)
    greet() {
        return "Hello, " + this.greeting;
    }
}

يمكننا تعريف المزخرف ‎@enumerable‎ باستخدام تصريح الدالة التالي:

function enumerable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.enumerable = value;
    };
}

المزخرف ‎@enumerable(false)‎ هنا مولِّد مزخرفات. عندما يستدعَى المزخرف ‎@enumerable(false)‎، ستُعدَّل الخاصيّةُ ‎enumerable‎ التابعة لواصفِ الخاصيّة .

مزخرفات دوال الوصول (Accessor Decorators)

يُصرَّح عن مزخرفات دوال الوصول مباشرة قبل التصريح عن دالة وصولٍ. يُطبَّق المزخرف على واصف الخاصية (Property Descriptor) الخاص بدالة الوصول (انظر توثيق الدالة ‎Object.defineProperty()‎‎‎ للاستزادة)، ويُمكن استخدامه لملاحظة تعريف دالة الوصول، أو تعديله، أو استبداله. لا يمكن استخدام مزخرف دالة وصول في ملف تصريحات (declaration file)، أو في أي سياق محيط (ambient context) آخر (مثل استخدامه على صنفٍ مصرَّح عنه بالكلمة المفتاحية ‎declare‎).

ملاحظة: تمنع TypeScript زخرفة كل من دالة الوصول ‎get‎ ودالة الوصول ‎set‎ لعنصرٍ واحد. عوضًا عن زخرفة كل منهما على حدة، يجب تطبيق جميع المزخرفات المرغوب تطبيقها على العنصر على أوّل دالة وصولٍ محدّدة حسب ترتيب الملفّ. هذا لأن المزخرفات تُطبَّق على واصف خاصية، والذي يدمج كلًّا من دالة الوصول ‎get‎ ودالة الوصول ‎set‎، ولا يطبَّق على كل تصريح على حدة.

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

  1. إمّا الدالة البانية (constructor function) الخاصّة بالصنف لعنصرٍ ساكن (static member)، أو سلسلة prototype الخاصة بالصنف لعنصر نسخةٍ (instance member).
  2. اسم العنصر.
  3. واصف الخاصيّة للعنصر.

ملاحظة: قيمة واصف الخاصية ستكون ‎undefined‎ إذا كنت تستهدف نسخًا أقدم من ‎ES5‎.

إذا أعاد مزخرف دالة الوصول قيمةً ما، فستُستخدم هذه القيمة كواصف خاصيّة للتابع.

ملاحظة: تُتَجاهل القيمة التي يُعيدها المزخرف إذا كنت تستهدف نسخًا أقدم من ‎ES5‎.

ما يلي مثال على مزخرف دالة وصول (‎@configurable‎) يُطبَّق على تابعٍ في الصنف ‎Point‎:

class Point {
    private _x: number;
    private _y: number;
    constructor(x: number, y: number) {
        this._x = x;
        this._y = y;
    }

    @configurable(false)
    get x() { return this._x; }

    @configurable(false)
    get y() { return this._y; }
}

يمكننا تعريف المزخرف ‎@configurable‎ باستخدام تصريح الدالة التالي:

function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    };
}

مزخرفات الخاصيات (Property Decorators)

يُصرَّح عن مزخرفات الخاصيات مباشرة قبل التصريح عن خاصية. لا يمكن استخدام مزخرف خاصيات في ملف تصريحات (declaration file)، أو في أي سياق محيط (ambient context) آخر (مثل استخدامه على صنفٍ مصرَّح عنه بالكلمة المفتاحية ‎declare‎).

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

  1. إمّا الدالة البانية (constructor function) الخاصّة بالصنف لعنصرٍ ساكن (static member)، أو سلسلة prototype الخاصة بالصنف لعنصر نسخةٍ (instance member).
  2. اسم العنصر.

ملاحظة: لا يُوفَّر واصف خاصيّة كمعامل لمزخرف خاصية بسبب آلية تهيئة مزخرفات الخاصيات في TypeScript. هذا لأنه لا يوجد آلية لوصف خاصية نسخةٍ عند تعريف عناصر سلسلة prototype، ولا يمكن ملاحظة أو تعديل مُهيِّئ خاصيةٍ ما. وتُتَجاهَل القيمة المعادة كذلك. لذا فلا يمكن استخدام مزخرف الخاصيات إلا لملاحظة ما إذا صُرِّح عن خاصيّة ذات اسم محدّد لصنف أو لا.

يمكننا استخدام هذه المعلومات لتسجيل بيانات وصفيّة (metadata) حول الخاصية كما في المثال التالي:

class Greeter {
    @format("Hello, %s")
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        let formatString = getFormat(this, "greeting");
        return formatString.replace("%s", this.greeting);
    }
}

يُمكننا بعدها تعريف المزخرف ‎@format‎ والدالة ‎getFormat‎ باستخدام تصريحات دوال كما يلي:

import "reflect-metadata";

const formatMetadataKey = Symbol("format");

function format(formatString: string) {
    return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
    return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}

المزخرف ‎@format("Hello, %s")‎ هنا مولِّدُ مزخرفاتٍ. عندما يُستدعى المزخرف ‎@format("Hello, %s")‎، فسيضيف خانة بيانات وصفيّة للخاصية باستخدام الدالة ‎Reflect.metadata‎ التي جاءت من مكتبة ‎reflect-metadata‎. عندما تُستدعى الدالة ‎getFormat‎ فإنّها تقرأ قيمة البيانات الوصفيّة وتعيدها.

ملاحظة: يتطلّب هذا المثال مكتبة ‎reflect-metadata‎. انظر قسم البيانات الوصفيّة أدناه للمزيد من المعلومات حول هذه المكتبة.

مزخرفات المعاملات (Parameter Decorators)

يُصرَّح عن مزخرفات المعاملات مباشرة قبل التصريح عن معامل. يُطبَّق مزخرف المعاملات على الدالة لدالة صنف بانيّة (class constructor) أو تصريح تابعٍ (method declaration). لا يمكن استخدام مزخرف خاصيات في ملف تصريحات (declaration file)، أو في حمل زائد (overload)، أو في أي سياق محيط (ambient context) آخر (مثل استخدامه على صنفٍ مصرَّح عنه بالكلمة المفتاحية ‎declare‎).

سيُستدعى تعبير مزخرف المعاملات كدالة أثناء التنفيذ مع تمرير المعاملات الثلاثة التالية:

  1. إمّا الدالة البانية (constructor function) الخاصّة بالصنف لعنصرٍ ساكن (static member)، أو سلسلة prototype الخاصة بالصنف لعنصر نسخةٍ (instance member).
  2. اسم العنصر.
  3. فهرس المعامل الترتيبيّ في قائمة معاملات الدالة.

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

تُتَجاهَل القيمة المعادة لمزخرف المعاملات.

ما يلي مثال على مزخرف معاملات (‎@required‎) يُطبَّق على معامل عنصرٍ من عناصر الصنف ‎Greeter‎:

class Greeter {
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }

    @validate
    greet(@required name: string) {
        return "Hello " + name + ", " + this.greeting;
    }
}

يُمكننا بعد ذلك تعريف المزخرفين ‎@required‎ و‎@validate‎ باستخدام تصريحي الدالتين التاليين:

import "reflect-metadata";

const requiredMetadataKey = Symbol("required");

function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
    let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
    existingRequiredParameters.push(parameterIndex);
    Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}

function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
    let method = descriptor.value;
    descriptor.value = function () {
        let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
        if (requiredParameters) {
            for (let parameterIndex of requiredParameters) {
                if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
                    throw new Error("Missing required argument.");
                }
            }
        }

        return method.apply(this, arguments);
    }
}

يُضيف المزخرف ‎@required‎ خانة بيانات وصفيّة تُعلِّم (mark) المعامل على أنّه مطلوب. يُحيط بعد ذلك المزخرف ‎@validate‎ التابعَ ‎greet‎ داخل دالّةٍ تتحقّق من المعاملات قبل استدعاء التابع الأصلي.

ملاحظة: يتطلّب هذا المثال مكتبة ‎reflect-metadata‎. انظر قسم البيانات الوصفيّة أدناه للمزيد من المعلومات حول هذه المكتبة.

البيانات الوصفيّة

تستعمل بعض الأمثلة أعلاه مكتبة ‎reflect-metadata‎، وهي مكتبة تُضيف دعمًا لواجهة برمجيّة تجريبيّة لإدارة البيانات الوصفيّة. هذه المكتبة ليست بعدُ جزءًا من معيار ECMAScript (JavaScript). لكن حالما تُتَبنَّى المزخرفات كجزء من معيار ECMAScript فستُقتَرَح هذه الإضافات للتبني لتكون جزءا من المعيار كذلك.

يُمكنك تثبيت هذه المكتبة عبر npm:

npm i reflect-metadata --save

تضيف TypeScript دعمًا تجريبيًّا لإخراج (أو توليد) بعض أنواع البيانات الوصفية للتصريحات المُزخرَفة. لتفعيل هذا الدعم التجريبي، يجب عليك ضبط خيار المترجم ‎emitDecoratorMetadata‎ إما على سطر الأوامر أو على ملفّ ‎tsconfig.json‎ الخاص بك:

سطر الأوامر:

tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata

ملفّ ‎tsconfig.json‎:

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

مادام الخيار مفعّلًا وما دامت مكتبة ‎reflect-metadata‎ مستوردَةً فستتوفَّر معلومات إضافيّة حول أنواع زمن التصميم (design-time type information) أثناء التنفيذ. يمكن فهم هذا أكثر بالمثال التالي:

import "reflect-metadata";

class Point {
    x: number;
    y: number;
}

class Line {
    private _p0: Point;
    private _p1: Point;

    @validate
    set p0(value: Point) { this._p0 = value; }
    get p0() { return this._p0; }

    @validate
    set p1(value: Point) { this._p1 = value; }
    get p1() { return this._p1; }
}

function validate<T>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) {
    let set = descriptor.set;
    descriptor.set = function (value: T) {
        let type = Reflect.getMetadata("design:type", target, propertyKey);
        if (!(value instanceof type)) {
            throw new TypeError("Invalid type.");
        }
        set(value);
    }
}

سيحقن مترجم TypeScript معلومات أنواع زمن التصميم باستخدام مزخرف ‎@Reflect.metadata‎. يُمكنك عدّ ما سبق مكافئًا لشيفرة TypeScript التالية:

class Line {
    private _p0: Point;
    private _p1: Point;

    @validate
    @Reflect.metadata("design:type", Point)
    set p0(value: Point) { this._p0 = value; }
    get p0() { return this._p0; }

    @validate
    @Reflect.metadata("design:type", Point)
    set p1(value: Point) { this._p1 = value; }
    get p1() { return this._p1; }
}

ملاحظة: بيانات المزخرفات الوصفيّة ميزةٌ تجريبيّة، وقد تُضيف تغييرات تكسر شيفرتك في النسخ المستقبلية.

مصادر