الوحدة 5 · إيصال TypeScript إلى الإنتاج
تصريحات using والـ Decorators القياسية
17 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
حرّر الموارد بموثوقية باستخدام تصريحات using وSymbol.dispose، واكتب decorators قياسية محدَّدة الأنواع للسلوك العابر للأجزاء مثل التسجيل وإعادة المحاولة.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
يحجز نظام الدفع في Cartwheel المخزون قبل الخصم من البطاقة، ثم يحرّر الحجز إن فشل أي شيء. النسخة الأولى من ذلك الكود بدت هكذا:
function placeOrder(skus: string[]) {
const reservations = skus.map((sku) => reserve(sku));
chargeCard(); // throws on a declined card
reservations.forEach((r) => r.release());
}البطاقة المرفوضة تتخطّى السطر الأخير، فيبقى المخزون مقفلًا حتى تلاحظه مهمة ليلية. الإصلاح التقليدي هو try/finally، متداخلة مرة لكل مورد. هذا الدرس يغطّي جواب اللغة المدمج، ثم ميزة حديثة ثانية تُبقي الكود العابر للأجزاء مثل إعادة المحاولة خارج منطق العمل: الـ decorators القياسية.
using: تنظيف مربوط بالنطاق
تصريح using مثل const، مع إضافة واحدة: حين تنتهي الكتلة التي يعيش فيها، بـ return أو بالوصول إلى نهايتها أو بـ throw، تستدعي JavaScript الـ method المسمّاة [Symbol.dispose]() على القيمة.
function reserve(sku: string): Disposable {
log(`reserve ${sku}`);
return { [Symbol.dispose]: () => log(`release ${sku}`) };
}
function placeOrder() {
using tent = reserve('TENT-2P');
using mug = reserve('MUG-SET');
chargeCard(); // even if this throws…
} // …both are released here: MUG-SET first, then TENT-2Pتصريحات using المتعددة تُحرَّر بترتيب معكوس، تمامًا مثل كتل finally المتداخلة، فيُحرَّر المورد دائمًا قبل الموارد التي أُنشئ بعدها. وإن رمى التحرير نفسه خطأً بينما خطأ آخر قيد الانتشار، يُحفظ الاثنان في SuppressedError بدل أن يحلّ أحدهما محلّ الآخر بصمت.
للتنظيف غير المتزامن، مثل إغلاق معاملة قاعدة بيانات، استخدم await using مع كائن فيه [Symbol.asyncDispose]() تُعيد promise. وحين يكون عدد الموارد ديناميكيًا، اجمعها في DisposableStack (أو AsyncDisposableStack)، وهي نفسها قابلة للتحرير:
function placeOrder(skus: string[]) {
using stack = new DisposableStack();
for (const sku of skus) stack.use(reserve(sku));
chargeCard();
} // every reservation released, in reverse orderوصلت using في TypeScript 5.2. وهي تعمل أصليًا في Node 24 وفي Chrome وEdge منذ الإصدار 134 وFirefox منذ 141، لكن Safari لا يدعمها بعد (حتى أواخر 2026)، فهي ليست Baseline. وللكود الذي يصل إلى المتصفّحات، دع TypeScript أو الـ bundler يحوّل الصياغة إلى صيغة أقدم، وتأكّد من وجود Symbol.dispose في بيئة التشغيل (يوفّره polyfill حيث يغيب). والأنواع موجودة في الـ lib المسمّاة esnext.disposable (المضمّنة في "lib": ["esnext"]). بيئة تجربة التمارين تستخدم الـ lib الخاصة بـ ES2022، فيعلن كود البداية هاتين القطعتين بنفسه، وهي معاينة لدمج التصريحات الذي ستتعلّمه في الدرس القادم.
جعل الـ APIs الخاصة بك قابلة للتحرير
تؤتي using أكبر ثمارها حين تُعيد الـ APIs الخاصة بك قيمًا قابلة للتحرير. الـ emitter من باعثات أحداث وبنّاؤون آمنون من حيث النوع أعاد دالة off؛ وإعادة كائن فيه [Symbol.dispose] أيضًا تتيح للمستدعين حصر الاشتراك في كتلة:
function subscribe<K extends keyof CheckoutEvents>(event: K, handler: (p: CheckoutEvents[K]) => void): Disposable {
const off = bus.on(event, handler);
return { [Symbol.dispose]: off };
}
async function waitForShipment(orderId: number) {
using _ = subscribe('order.shipped', (e) => { if (e.orderId === orderId) notify(e); });
await pollUntilShipped(orderId);
} // listener removed here, however the function endsالمرشّحون الجيدون كل ما له “تراجع” مقابل: الأقفال والحجوزات، واشتراكات الأحداث، والمؤقّتات، والملفات المؤقتة، ومعاملات قواعد البيانات (مع await using). إن وجدت نفسك تكتب try/finally فقط لاستدعاء method تنظيف، فذلك المورد يريد [Symbol.dispose].
الـ decorators القياسية
الـ decorator (المُزخرِف) دالة تُطبَّق بـ @ على صنف أو عضو في صنف. نفّذ TypeScript 5.0 الـ decorators القياسية (TC39)، ويترجمها TypeScript إلى استدعاءات دوال عادية، فتعمل في أي بيئة تشغيل. يتلقّى method decorator الـ method الأصلية وكائن context، ويمكنه إعادة بديل:
function logged<This, Args extends unknown[], Return>(
method: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>,
) {
const name = String(context.name);
return function (this: This, ...args: Args): Return {
console.log(`→ ${name}`, args);
return method.call(this, ...args);
};
}
class PaymentGateway {
@logged
charge(amountCents: number, currency: string) { /* … */ }
}الـ generics تجعل الـ decorator يعمل على أي method دون any: تُستنتج This وArgs وReturn من الـ method التي يزخرفها، ويحتفظ البديل بالتوقيع نفسه. والـ decorators التي تأخذ خيارات هي مصانع decorators (decorator factories)، أي دوال تُعيد الـ decorator:
function retry(times: number) {
return function <This, Args extends unknown[], Return>(
method: (this: This, ...args: Args) => Return,
_context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>,
) {
return function (this: This, ...args: Args): Return {
for (let attempt = 1; ; attempt++) {
try {
return method.call(this, ...args);
} catch (e) {
if (attempt >= times) throw e;
}
}
};
};
}
class PaymentGateway {
@retry(3)
charge(amountCents: number, currency: string) { /* … */ }
}الـ decorators المكدّسة تُطبَّق من الأسفل إلى الأعلى. مع @logged فوق @retry(3)، يُطبَّق غلاف إعادة المحاولة على charge أولًا ثم يغلّف logged النتيجة، فتحصل على سطر تسجيل واحد لكل استدعاء لا لكل محاولة. بدّلهما فتسجّل كل محاولة. لا أحد منهما خاطئ، لكن الترتيب سلوك، فاختره عن قصد.
ويقدّم context أيضًا context.name وcontext.static وcontext.private وcontext.addInitializer(fn)، التي تنفّذ كودًا حين تُنشأ نسخة (وهي الطريقة المعتادة لكتابة decorator مثل @bound يربط method بنسختها).
في التمرين ستحرّر حجوزات المخزون بـ using وتضيف @retry محدَّد النوع إلى بوابة الدفع في Cartwheel. الدرس القادم: وصف كود لم تكتبه، بملفات التصريحات.
أهم النقاط
- تصريح
usingيستدعي[Symbol.dispose]()على القيمة حين تنتهي الكتلة المحيطة، سواء عادت بشكل طبيعي أو رمت خطأً. - تصريحات
usingالمتعددة في كتلة واحدة تُحرَّر بترتيب معكوس، مثل كتلtry/finallyالمتداخلة. await usingوSymbol.asyncDisposeيفعلان الشيء نفسه للتنظيف الذي يُعيد promise.- الـ decorators القياسية (منذ TypeScript 5.0) دوال بالشكل
(value, context)يمكنها إعادة بديل؛ وهي ليست نفسهاexperimentalDecoratorsالأقدم. - حدّد نوع method decorator بـ
ClassMethodDecoratorContextومعاملات generic هيThisوArgsوReturnكي يعمل على أي method دونany.