مقدمة: لماذا esModuleInterop ليس مجرد علامة اختيار في tsconfig
\n\nإذا كنت قد كتبت يومًا ما بلغة TypeScript وقمت باستيراد مكتبة مثل express أو lodash، فمن المحتمل أنك واجهت موقفًا حيث يشتكي المترجم من import * as express from 'express'. أو على العكس، تستخدم بهدوء import express from 'express'، ولكن في كود JavaScript المُنشأ ترى بنية غريبة مثل module.exports.default. ما هذا السحر؟ السحر (أو عدم وجوده) هو العلمة esModuleInterop في ملف tsconfig.json الخاص بك.
هذه المعلمة هي واحدة من أكثر الأسئلة شيوعًا على Stack Overflow (آلاف المشاهدات!)، وليس من المستغرب: بدون فهمها، يمكنك بسهولة الوقوع في فخ عدم توافق الوحدات. في هذا البرنامج التعليمي، لن نشرح النظرية فحسب، بل سننتقل من المشكلة إلى الحل مع أمثلة حية. هيا بنا!
\n\nالمشكلة: صراع عالمين من الوحدات
\n\nTypeScript هي طبقة فوق JavaScript، وتاريخيًا، لدى JavaScript نظامان رئيسيان للوحدات:
\n- \n
- CommonJS (CJS) — معيار Node.js، يستخدم
require()وmodule.exports. مثال:const express = require('express');\n - ES Modules (ESM) — المعيار الحديث، يستخدم
importوexport. مثال:import express from 'express';\n
المشكلة هي أن هذه الأنظمة تتعامل بشكل مختلف مع التصدير الافتراضي (default export). في ESM، يمكن أن تحتوي الوحدة على export default واحد، ويتم استيرادها كـ import name from 'module'. في CommonJS، لا يوجد مفهوم "التصدير الافتراضي" — يوجد فقط كائن module.exports الذي يمكن أن يكون أي شيء.
عندما يقوم TypeScript بتجميع الكود الخاص بك إلى CommonJS (وهو أمر مطلوب غالبًا لـ Node.js)، يجب عليه أن يقرر: كيف يمثل import x from 'y' كـ require()؟ بدون esModuleInterop، يقوم TypeScript بذلك حرفيًا: ينشئ module.exports.default، مما يعطل استيراد المكتبات المكتوبة تحت CommonJS.
ما هو esModuleInterop؟
\n\nesModuleInterop هي علامة في tsconfig.json تقوم بتمكين توافق إضافي بين CommonJS و ES Modules. عندما يتم تعيينها على true، يقوم TypeScript بإنشاء كود مساعد يسمح لك بكتابة import foo from 'bar' حتى لوحدات CommonJS التي لا تحتوي على تصدير default.
افتراضيًا في TypeScript 4.x والإصدارات الأحدث، هذه العلامة تساوي true (إذا كنت تستخدم --init)، ولكن في المشاريع القديمة غالبًا ما كانت معطلة. لماذا؟ لأنها تغير سلوك الاستيراد، وبدون فهم ذلك يؤدي إلى أخطاء.
كيف يبدو ذلك في tsconfig.json
\n{\n "compilerOptions": {\n "esModuleInterop": true,\n // إعدادات أخرى...\n }\n}\n\n\nتمكين هذه العلامة يعمل أيضًا تلقائيًا على تمكين allowSyntheticDefaultImports، الذي يسمح بالاستيراد الافتراضي من الوحدات التي لا تحتوي على تصدير default فعلي على مستوى الأنواع.
برنامج تعليمي خطوة بخطوة: من المشكلة إلى الحل
\n\nدعنا نحلل الموقف بمثال حقيقي. لنفترض أن لدينا مكتبة بسيطة على CommonJS:
\n\n// myLib.js (CommonJS)\nmodule.exports = function greet(name) {\n return `Hello, ${name}!`;\n};\n\n\nنريد استخدامها في مشروع TypeScript. لننشئ ملف index.ts:
// index.ts\nimport greet from './myLib';\nconsole.log(greet('World'));\n\n\nالآن دعنا نرى ما سيحدث عند التجميع بإعدادات مختلفة لـ esModuleInterop.
الخطوة 1: بدون esModuleInterop (false)
\n\nلنقم بتشغيل التجميع مع "esModuleInterop": false. سيعطي TypeScript خطأ:
Module '"myLib"' has no d