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

Declare في TypeScript: شرح ملفات التعريفات .d.ts

ملف .d.ts يصف أنواع كود JavaScript دون أن يحتوي على أي جزء منه، والكلمة declare تفعل الشيء نفسه داخل ملف .ts. تعرّف على طريقة توليد ملفات التعريفات، ودور حزم @types، وكيف تحدد أنواع وحدة بلا أنواع، وكيف يوسّع declare global وتوسيع الوحدات الأنواع الموجودة.

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

ملف التعريفات (.d.ts) يحتوي على أنواع ولا شيء غيرها: توقيعات وواجهات وأشكال أصناف لكود JavaScript موجود في مكان آخر. والكلمة declare تؤدي المهمة نفسها داخل ملف عادي. تقول للمترجم "هذا موجود وقت التشغيل، ثق بي"، ولا تُخرج أي كود.

قبل المترجم __APP_VERSION__.length لأن التعريف يقول إنه نص. وقت التشغيل لا يوجد متغير كهذا، فترمي قراءة .length الخطأ ReferenceError: __APP_VERSION__ is not defined، وهو استثناء وقت تشغيل لم يتوقعه المترجم. هذا هو عقد كل تعريف: الأنواع صحيحة بقدر صحة الـ JavaScript الذي خلفها.

ما الذي يوضع في ملف .d.ts

الترجمة مع declaration: true توضح الفكرة أفضل من غيرها. من هذا المصدر:

// price.ts
export interface LineItem {
    name: string;
    price: number;
    qty: number;
}

const TAX = 0.2;

export function total(items: LineItem[]) {
    const sum = items.reduce((acc, item) => acc + item.price * item.qty, 0);
    return Math.round(sum * (1 + TAX) * 100) / 100;
}

export class Cart {
    private items: LineItem[] = [];
    add(item: LineItem) {
        this.items.push(item);
        return this;
    }
}

يكتب tsc الملف price.js وهذا الملف price.d.ts:

export interface LineItem {
    name: string;
    price: number;
    qty: number;
}
export declare function total(items: LineItem[]): number;
export declare class Cart {
    private items;
    add(item: LineItem): this;
}

اختفت أجسام الدوال، وكُتبت أنواع الإعادة المستنتجة صراحة (number، this)، واحتفظ الحقل الخاص باسمه لكنه فقد نوعه، ولا وجود أصلًا للثابت TAX غير المصدَّر. تنشر المكتبة ملف .js من أجل Node وملف .d.ts من أجل محررك والمترجم. ويُنتج emitDeclarationOnly: true ملفات .d.ts فقط، للمشاريع التي تبني فيها أداة تجميع الـ JavaScript.

الواجهات المدمجة في ES2022 التي تستخدمها كل يوم تأتي أيضًا من ملفات تعريفات: lib.es2022.d.ts وأخواتها تأتي مع TypeScript ويختارها الخياران target وlib.

أشكال declare

كل عبارة declare تصف شيئًا موجودًا أصلًا. في ملف .d.ts ليس فيه import أو export (ملف تعريفات عام)، يصبح كل واحد من هذه مرئيًا للمشروع كله:

// globals.d.ts
declare const API_URL: string;                     // a global constant
declare let debugMode: boolean;                    // a global variable
declare function track(event: string, props?: Record<string, string>): void;
declare class Widget {                             // a class from a script tag
    constructor(el: string);
    render(): void;
}
declare namespace Analytics {                      // a global object
    function page(name: string): void;
}
declare module "legacy-charts" {                   // a module you import
    export function draw(data: number[]): void;
}

داخل ملف .d.ts لا يحتاج interface وtype إلى declare؛ وأي تعريف آخر على المستوى الأعلى دون declare أو export هو الخطأ TS1046. وما إن يصبح في الملف import أو export حتى تصبح تعريفاته محلية فيه، وتوضع الإضافات إلى النطاق العام في كتلة declare global (موضحة لاحقًا). والتوقيع المعرَّف هو ما يفحص المترجم الاستدعاءات بمقارنتها به:

يبلّغ المترجم عن index.ts(5,21): error TS2322: Type 'number' is not assignable to type 'string'. مرّر { items: "3" }، أو غيّر التعريف إذا كانت الدالة الحقيقية تقبل أرقامًا.

حزم @types

كثير من حزم npm تأتي بملفات .d.ts خاصة بها، يُشار إليها من الحقل types (أو شرط types في exports) في ملف package.json الخاص بها. وللحزم المكتوبة بـ JavaScript فقط، ينشر مشروع DefinitelyTyped الذي يديره المجتمع أنواعًا تحت النطاق @types:

npm i lodash
npm i -D @types/lodash

عندما تستورد حزمة بـ import يبحث TypeScript تلقائيًا عن node_modules/@types/{name}. أما الأنواع التي تصف متغيرات عامة لا استيرادًا، مثل @types/node (process، Buffer) أو describe وit الخاصة بأداة تشغيل الاختبارات، فيجب ذكرها في tsconfig.json:

{
    "compilerOptions": {
        "types": ["node"]
    }
}

دون هذا المدخل لا يحمّلها TypeScript 7، حتى لو كانت الحزمة مثبتة:

error TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try `npm i --save-dev @types/node` and then add 'node' to the types field in your tsconfig.

يكتب tsc --init القيمة "types": [] في الإعداد الجديد، مع تعليق يقترح ["node"] لمشاريع Node.

تحديد أنواع وحدة بلا أنواع

استيراد حزمة JavaScript ليس لها أنواع، لا مضمّنة ولا في @types، هو الخطأ TS7016:

error TS7016: Could not find a declaration file for module 'fakelib'. '/project/node_modules/fakelib/index.js' implicitly has an 'any' type.

أصلحه بملف .d.ts في أي مكان من مشروعك (المجلد types/ شائع؛ يكفي أن يشمله include) ليس فيه import أو export على المستوى الأعلى. صِف الأجزاء التي تستخدمها:

// types/fakelib.d.ts
declare module "fakelib" {
    export function hi(name: string): string;
    export const version: string;
}

// Non-code files a bundler lets you import
declare module "*.svg" {
    const url: string;
    export default url;
}

أقصر صيغة، declare module "fakelib"; في سطر واحد، تجعل كل استيراد من الحزمة any. تزيل الخطأ وتزيل معه كل فحص، لذلك تعامل معها كخطوة مؤقتة.

declare global

الكود الذي يضيف إلى النطاق العام، مثل polyfill، أو دالة جديدة على نوع مدمج، أو متغير عام يضبطه وسم script، يحتاج إلى الأنواع المطابقة. يضيفها declare global. ويجب أن يكون داخل وحدة (ملف فيه import أو export)، ولهذا يوجد export {} في أول الملف:

الواجهة interface Array<T> تندمج مع واجهة Array المدمجة بدلًا من أن تحل محلها، وvar (لا let ولا const) هو ما يضيف خاصية إلى globalThis. توسيع prototypes الأنواع المدمجة محفوف بالمخاطر في الكود المشترك؛ وتقنية declare global نفسها هي ما تستخدمه المشاريع لإضافة متغيراتها إلى process.env في الواجهة NodeJS.ProcessEnv من @types/node.

توسيع الوحدات (Module Augmentation)

للإضافة إلى أنواع حزمة تستوردها، أعد تعريف اسم وحدتها وافتح الواجهة من جديد. يجب أن يكون الملف نفسه وحدة (الـ import يحقق ذلك؛ وexport {} يعمل أيضًا). دون import أو export تعرّف الكتلة نفسها وحدة config-lib جديدة كليًا تحجب أنواع الحزمة الحقيقية:

// types/config-lib.d.ts
import "config-lib";

declare module "config-lib" {
    interface Settings {
        beta: boolean; // merged into the package's own Settings interface
    }
}

بعد ذلك يصبح نوع load().beta من config-lib هو boolean في كل مكان. الواجهات تندمج؛ أما أسماء الأنواع البديلة (type aliases) فلا تندمج، لذلك يجب أن تصدّر المكتبة interface حتى يعمل هذا. وهكذا تضيف الإضافات (plugins) حقولًا إلى كائنات الطلب أو الإعدادات في إطار عمل ما.

skipLibCheck

يمنع skipLibCheck: true المترجم من فحص أنواع ملفات .d.ts، بما فيها تلك الموجودة في node_modules. ويظل كودك يُفحص بمقارنته بها. هذا يوفر الوقت ويتجنب أخطاء حزمتين تتعارض تعريفاتهما، ولهذا يفعّله tsc --init. والثمن أن الخطأ داخل ملفات .d.ts التي كتبتها أنت لا يُبلّغ عنه أيضًا.

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

ما هو ملف .d.ts في TypeScript؟

ملف تعريفات: يحتوي على أنواع فقط (توقيعات الدوال، والواجهات، وأشكال الأصناف) دون أي تنفيذ. يصف كود JavaScript موجودًا في مكان آخر، مثل مكتبة مُترجمة أو واجهات المتصفح المدمجة، حتى يستطيع TypeScript فحص الكود الذي يستخدمه. ولا يُخرج tsc أي JavaScript منه.

ماذا تفعل الكلمة declare في TypeScript؟

تخبر المترجم أن قيمة ما موجودة وقت التشغيل دون أن تنشئها. declare const VERSION: string; يتيح لك استخدام VERSION كنص، ويختفي السطر من الناتج. إذا لم يعرّف أي شيء VERSION فعلًا، يفشل البرنامج وقت التشغيل بـ ReferenceError.

كيف أصلح الخطأ "Could not find a declaration file for module"؟

الخطأ TS7016 يعني أن الحزمة ليس لها أنواع. ثبّتها إن وُجدت (npm i -D @types/package-name)، أو أضف ملف .d.ts فيه declare module "package-name" { ... } يصف ما تستخدمه. أما declare module "package-name"; وحده فيُسكت الخطأ بجعل نوع الوحدة كلها any.

كيف أولّد ملفات .d.ts من TypeScript؟

اضبط "declaration": true في tsconfig.json (أو مرّر --declaration). عندها ينتج كل ملف .ts ملف .d.ts بجوار ملف .js الخاص به. أضف emitDeclarationOnly عندما تبني أداة أخرى الـ JavaScript، وdeclarationMap حتى تستطيع المحررات الانتقال من الأنواع إلى كودك المصدري.

ما الفرق بين declare global وdeclare module؟

declare global { ... } يضيف إلى النطاق العام، مثل خاصية جديدة على Array أو متغير عام. أما declare module "name" { ... } فيصف وحدة تستوردها بذلك الاسم. يجب أن يكون declare global داخل وحدة (ملف فيه import أو export)؛ وdeclare module يعرّف وحدة جديدة في ملف ليس فيه استيراد أو تصدير، ويوسّع وحدة موجودة إذا كان داخل وحدة.

Coddy programming languages illustration

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

ابدأ الآن