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

الـ Decorators في TypeScript: للتوابع والأصناف والحقول

الـ decorators دوال تغلّف أعضاء الصنف أو تستبدلها بصيغة @. تعرّف على الـ decorators القياسية التي تدعمها TypeScript دون أي خيار (للصنف والتابع والـ getter والحقل والـ accessor)، ومصانع الـ decorators، وaddInitializer، وكيف تختلف عن experimentalDecorators القديمة التي تستخدمها Angular وNestJS.

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

الـ decorator دالة تربطها بصنف أو بعضو في صنف بصيغة @name. تستقبل التابع الأصلي (أو الصنف، أو الحقل) مع كائن سياق يصفه، ويمكنها إعادة بديل. تدعم TypeScript الـ decorators القياسية دون أي خيار في المترجم.

يعمل @logged مرة واحدة عند تعريف الصنف، ويستبدل add بالدالة المغلّفة التي يعيدها. كل استدعاء يمر عبر هذه الدالة المغلّفة. تحافظ معاملات النوع العامة على أنواع this والوسائط والقيمة المُعادة للتابع، فيظل add يأخذ رقمين ويعيد رقمًا.

كيف تُترجم الـ Decorators

تأتي الـ decorators القياسية من مقترح TC39 للغة JavaScript، وتطبّقها TypeScript منذ الإصدار 5.0. المقترح ليس جزءًا من معيار JavaScript بعد، وNode 24 لا يحلل صيغة @، لذلك حين يكون الهدف ES2022 (كما في هذه الصفحات) يعيد المترجم كتابة كل صنف مزخرف إلى JavaScript عادية تستدعي دوال مساعدة (__esDecorate و__runInitializers، تُولَّد في أول الملف). يعمل الناتج في أي مكان تعمل فيه ES2022.

نتيجة لذلك: لا يمكن تشغيل ملف فيه decorators مع إزالة الأنواع المدمجة في Node (node file.ts)، لأنها تحذف الأنواع فقط وتترك @ في مكانه. يتوقف Node بالخطأ SyntaxError: Invalid or unexpected token. ترجمه أولًا بـ tsc أو بأداة تجميع.

أنواع الـ Decorators وتوقيعاتها

لكل decorator قياسي الشكل (value, context) => replacement | void. ما تكونه value، وما يمكنك إعادته، يعتمد على ما تزخرفه:

يزخرفvalueنوع السياقما يُعاد
صنفالصنفClassDecoratorContextصنف بديل، أو لا شيء
تابعالتابعClassMethodDecoratorContextتابع بديل
getter / setterالـ getter أو الـ setterClassGetterDecoratorContext / ClassSetterDecoratorContextgetter أو setter بديل
حقلundefinedClassFieldDecoratorContextدالة تحوّل القيمة الابتدائية
حقل accessor{ get, set }ClassAccessorDecoratorContext{ get?, set?, init? }

لكل كائن سياق الخصائص kind وname وaddInitializer. ولسياق عضو الصنف أيضًا static وprivate وكائن access لقراءة العضو من نسخة. وتعمل الـ decorators على الأعضاء الثابتة وأعضاء #private أيضًا.

مصانع الـ Decorators

لتمرير خيارات، اكتب دالة تعيد decorator واستدعها في موضع @. هذا مصنع decorators:

يستدعي @retry(3) الدالة retry أولًا، والدالة التي تعيدها هي الـ decorator الفعلي. وتتراكم الـ decorators المتعددة: في @a @b method() يُطبَّق b أولًا ثم يغلّف a النتيجة.

decorators الأصناف وaddInitializer

يستقبل decorator الصنف الصنفَ نفسه. يمكنه إعادة صنف فرعي ليحل محله، أو ألا يعيد شيئًا ويكتفي بتسجيله في مكان ما. يسجّل context.addInitializer كودًا يعمل في لحظة محددة: لـ decorator الصنف، مباشرة بعد اكتمال تعريف الصنف؛ ولـ decorator التابع، عند إنشاء كل نسخة.

يوضع decorator الصنف قبل تعريف الصنف (أو بعد export). دون @bound كان استدعاء loose() سيرمي خطأ، لأن this ستكون undefined.

decorators الحقول والـ Accessors

لا يستطيع decorator الحقل رؤية عمليات الإسناد اللاحقة أو اعتراضها: قيمة value هي undefined، وكل ما يمكنه إعادته دالة تحوّل القيمة الابتدائية للحقل. لاعتراض القراءة والكتابة، عرّف الحقل بالكلمة المفتاحية accessor، التي تحوّله إلى زوج getter وsetter يعتمد على تخزين خاص، وزخرفه:

accessor جزء من المقترح نفسه. يولّد getter وsetter حقيقيين مبنيين على حقل #private، ولهذا يمر p.price = -5 عبر set الخاص بالـ decorator.

القياسية مقابل experimentalDecorators القديمة

قبل TypeScript 5.0 كانت الـ decorators الوحيدة في TypeScript نسخة مبكرة من المقترح، تُفعَّل بـ experimentalDecorators. هذا الخيار لا يزال موجودًا، ويحوّل المترجم إلى النموذج القديم بتوقيعات ودلالات مختلفة:

القياسية (دون خيار)القديمة (experimentalDecorators)
التوقيع(value, context)(target, propertyKey, descriptor)
كيف تغيّر التابعتعيد دالة جديدةتعدّل descriptor.value
decorators المعاملاتغير مدعومة (TS1206)مدعومة
emitDecoratorMetadataغير مدعوممدعوم (معلومات الأنواع وقت التشغيل عبر reflect-metadata)
decorators لـ accessor ({ get, set, init })، وaddInitializerنعملا
تعتمد علىمقترح TC39مسودة أقدم منه

الـ decorator المكتوب لنموذج لا يجتاز فحص الأنواع في الآخر. هذا decorator بالأسلوب القديم في مشروع دون الخيار:

index.ts(11,5): error TS1241: Unable to resolve signature of method decorator when called as an expression.
  The runtime will invoke the decorator with 2 arguments, but the decorator expects 3.

الحل إما إعادة كتابته بالصيغة القياسية (value, context)، كما في المثال الأول في هذه الصفحة، وإما تفعيل النموذج القديم للمشروع كله:

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

لا تزال Angular وNestJS وTypeORM تضبط experimentalDecorators في الإعدادات التي تولّدها أدواتها وتطلبها وثائقها. وتحتاج NestJS وTypeORM أيضًا إلى emitDecoratorMetadata، لأنهما تقرآن الأنواع وقت التشغيل: NestJS لحقن معاملات دالة البناء، وTypeORM لربط الخصائص بالأعمدة:

// Legacy model: needs experimentalDecorators (and emitDecoratorMetadata for DI).
@Injectable()
class UsersService {
    constructor(@Inject(DB) private db: Database) {}
}

إن كنت تستخدم أحد هذه الأطر، فاكتب الـ decorators بالطريقة القديمة واتبع وثائقه. وللكود الجديد دون إطار كهذا، استخدم الـ decorators القياسية.

متى تستخدم الـ Decorators

تناسب الـ decorators السلوك العابر الذي كان سيتكرر في توابع كثيرة: التسجيل في السجلات، وقياس الزمن، والتخزين المؤقت، وإعادة المحاولة، وفحوص الصلاحيات، والتحقق، وتسجيل الأصناف في حاوية أو موجّه. لكنها تخفي مسار التحكم أيضًا، إذ يجب على القارئ أن يبحث عما يفعله @retry قبل أن يعرف ما يفعله التابع. لاستخدام أو استخدامين، تكون دالة عليا عادية (const fetchData = retry(3, rawFetch)) أبسط وتعمل خارج الأصناف أيضًا.

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

ما هي الـ decorators في TypeScript؟

الـ decorator دالة تُطبَّق على صنف أو عضو في صنف بصيغة @name. تستقبل الشيء المزخرف وكائن سياق، ويمكنها إعادة بديل: تابع مغلّف، أو صنف جديد، أو دالة تحوّل القيمة الابتدائية لحقل. من استخداماتها الشائعة التسجيل في السجلات، والتحقق، والتخزين المؤقت، وتسجيل الأصناف.

هل أحتاج إلى experimentalDecorators لاستخدام الـ decorators في TypeScript؟

لا. منذ TypeScript 5.0 تعمل الـ decorators القياسية (TC39) دون أي خيار. أما experimentalDecorators فيحوّل المترجم إلى نموذج الـ decorators القديم، الذي بُنيت عليه أطر عمل مثل Angular وNestJS. يستخدم النموذجان توقيعات دوال مختلفة ولا يمكن تبديل أحدهما بالآخر.

ما الفرق بين الـ decorators القياسية وexperimentalDecorators؟

الـ decorators القياسية تستقبل (value, context) وتعيد بديلًا. أما القديمة فتستقبل (target, propertyKey, descriptor) وتعدّل واصف الخاصية. النموذج القديم وحده يدعم decorators المعاملات وemitDecoratorMetadata؛ والنموذج القياسي وحده فيه decorators لـ accessor تعيد { get, set, init } وcontext.addInitializer.

هل تدعم TypeScript الـ decorators على المعاملات؟

فقط مع تفعيل experimentalDecorators. في الوضع القياسي يكون الـ decorator على معامل هو الخطأ TS1206: Decorators are not valid here، لأن مقترح TC39 لا يتضمن decorators للمعاملات. لذلك تحتاج أطر حقن التبعيات التي تزخرف معاملات دالة البناء إلى الخيار القديم.

بأي ترتيب تُطبَّق عدة decorators؟

تُقيَّم تعبيرات الـ decorators بترتيب كتابتها، لكنها تُطبَّق بالترتيب العكسي: في @a @b method() يغلّف b التابع أولًا ثم يغلّف a النتيجة. لذلك يعمل a في الطبقة الخارجية عند استدعاء التابع.

Coddy programming languages illustration

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

ابدأ الآن