الوحدة 5 · إيصال TypeScript إلى الإنتاج
ملفات التصريحات وتوسيع الوحدات
16 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
صِف كود JavaScript لم تكتبه بملفات التصريحات، وحدّد أنواع المتغيّرات العامة ومتغيّرات البيئة، ووسّع الـ interfaces والوحدات الموجودة بدمج التصريحات وتوسيع الوحدات.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
ليس كل ما يلمسه كود TypeScript لديك مكتوبًا بـ TypeScript. نظام الدفع في Cartwheel يحمّل مقتطف تحليلات بوسم <script>، ويستخدم حاسبة شحن داخلية قديمة مكتوبة بـ JavaScript، ويقرأ متغيّرات البيئة عبر Vite، ويعمل بـ lib متأخّرة سنة عن المتصفّحات التي يعمل عليها. في كلٍّ من هذه الحالات، تحتاج إلى إخبار الـ compiler عن كود لا يستطيع رؤيته. وهذا ما وُجدت له ملفات التصريحات وdeclare.
declare: إنه موجود، ثق بي
declare تصف شيئًا موجودًا وقت التشغيل لكنه معرَّف في مكان آخر. لا تنتج أي JavaScript. والملف المنتهي بـ .d.ts لا يحتوي إلا مثل هذه التصريحات:
type AnalyticsEvent = 'checkout_started' | 'order_placed' | 'payment_failed';
declare const cartwheelAnalytics: {
track(event: AnalyticsEvent, props?: Record<string, string | number>): void;
};هذا الملف ليس فيه import ولا export، فهو script عام: كل ما فيه مرئي في كل ملف من المشروع. والآن صارت cartwheelAnalytics.track('order_plaecd') خطأ ترجمة، وهذا ما لم تنجح فيه وثائق المقتطف أبدًا.
التصريح وعدٌ غير مفحوص، تمامًا مثل تحويلات as في الأقسام السابقة. إن اختلف الـ API الحقيقي للمقتطف، سيثق TypeScript بنسختك بسرور. أبقِ التصريحات صغيرة، ولا تكتب إلا الأجزاء التي تستخدمها، وضع تعليقًا بإصدار المكتبة الذي تحقّقت منها مقابله.
تحديد أنواع حزمة JavaScript
قبل كتابة أي تصريحات لحزمة، تحقّق من المكانين اللذين تأتي منهما الأنواع عادة. حزم كثيرة تأتي بأنواعها الخاصة ("types" في package.json الخاص بها، أو ملفات .d.ts بجوار JavaScript). ولغيرها، يصون المجتمع حزم @types/ على DefinitelyTyped: npm install -D @types/legacy-shipping-calc إن وُجدت.
ولحزمة داخلية ليس لها أيٌّ منهما، أعلن الوحدة بنفسك:
declare module 'legacy-shipping-calc' {
export type Quote = { carrier: string; feeCents: number; days: number };
export function quote(country: string, weightGrams: number): Quote[];
}الصيغة المختصرة declare module 'legacy-shipping-calc'; دون جسم تعمل أيضًا، وتجعل كل import من نوع any. اعتبرها مخرج طوارئ مؤقتًا أثناء الترحيل، لا حلًا.
أبقِ التصريحات المكتوبة يدويًا معًا في مجلد واحد، مثل types/، وتأكّد أن include في tsconfig يشمله؛ فملف .d.ts لا يراه الـ compiler أبدًا لا يفعل شيئًا، ورسالة الخطأ الناتجة “Could not find a declaration file for module” مربكة في المرة الأولى. ولاحظ أيضًا أن هذا الملف عام، مثل ملف التحليلات: كتلة declare module 'name' باسم نصي هي طريقتك لوصف حزمة من ملف script.
دمج التصريحات
بعض التصريحات التي لها الاسم نفسه تتّحد بدل أن تتصادم. أكثر ما ستستخدمه: الـ interfaces تندمج.
interface CheckoutConfig { currency: string }
interface CheckoutConfig { giftWrap: boolean }
const config: CheckoutConfig = { currency: 'EGP', giftWrap: true }; // both requiredالأسماء المستعارة للأنواع لا تندمج أبدًا؛ تصريحان type CheckoutConfig يعطيان خطأ “Duplicate identifier”. هذا هو الفرق العملي الرئيسي بين type وinterface، ولهذا تكشف المكتبات التي تتوقّع أن تُوسَّع عن interfaces.
والدمج هو طريقتك لسدّ الفجوات في المكتبة القياسية. بيئة التجربة في هذه الدورة تستخدم الـ lib الخاصة بـ ES2022، فـ Array.prototype.findLast (من ES2023) غائبة عن أنواعها رغم أن كل متصفّح حالي يملكها. والدمج في الـ interface العام Array<T> يضيفها:
interface Array<T> {
findLast(predicate: (value: T, index: number, array: T[]) => unknown): T | undefined;
}في مشروع حقيقي، الإصلاح الأفضل هو الإعداد الصحيح لـ lib أو target. أما الدمج فللحالات التي لا تستطيع فيها الإعدادات التعبير عمّا تملكه بيئة التشغيل فعلًا، مثل polyfill تضمّنه بنفسك.
وهو أيضًا الطريقة التي يحدّد بها Vite أنواع متغيّرات البيئة لديك. أنواع العميل فيه تعلن interface عامًا باسم ImportMetaEnv، ويندمج مشروعك فيه:
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_PAYMENTS_KEY: string;
}توسيع الوحدات
للإضافة إلى نوع يعيش داخل وحدة، افتح الوحدة بـ declare module من ملف هو نفسه وحدة:
import 'cartwheel-ui';
declare module 'cartwheel-ui' {
interface Theme {
brand: { primary: string; onPrimary: string };
}
}صار للـ interface المسمّى Theme داخل cartwheel-ui حقل brand في كل مكان يُستخدم فيه. وللإضافة إلى النطاق العام من ملف وحدة، استخدم declare global { interface Window { dataLayer: unknown[] } }.
في التمرين ستعلن متغيّر التحليلات العام، وتضيف findLast إلى الـ lib في بيئة التجربة، وتوسّع interface إعداد بالدمج. الدرس الأخير: نقل قاعدة كود JavaScript كاملة.
أهم النقاط
- ملف
.d.tsلا يحتوي إلا أنواعًا؛ وdeclareتصف شيئًا موجودًا وقت التشغيل لكنه معرَّف في مكان آخر. - تحقّق من وجود أنواع مدمجة أو حزمة
@types/قبل كتابة التصريحات، ولا تكتب إلا أجزاء المكتبة التي تستخدمها. - الـ interfaces التي لها الاسم نفسه في النطاق نفسه تندمج في واحد؛ أما الأسماء المستعارة للأنواع فلا تندمج أبدًا، ولهذا تكون أنواع المكتبات القابلة للتوسيع interfaces.
declare module 'pkg' { … }في ملف فيه import أو export توسّع تلك الوحدة؛ وفي ملف ليس فيه أيٌّ منهما تعلن وحدة محيطة جديدة كليًا.declare global { … }داخل وحدة تضيف إلى النطاق العام، مثل خاصية علىWindowأو حقل فيImportMetaEnv.