require(x) vs import x: لماذا يمثل هذا صداعًا دائمًا لمطوري TypeScript
\n\nإذا سبق لك أن كتبت كودًا بلغة TypeScript ورأيت خطأ مثل Cannot use import statement outside a module أو require is not defined — فأنت لست وحدك. على موقع Stack Overflow، سؤال "الفرق بين 'require(x)' و 'import x'" حصد عشرات الآلاف من المشاهدات. وهذا ليس من قبيل الصدفة: الخلط بين نظامي الوحدات (modules) هو أحد أكثر الأخطاء شيوعًا التي يقع فيها حتى المطورون ذوو الخبرة.
في هذا الدليل التعليمي، لن نقتصر على سرد الاختلافات فقط. بل سنأخذك في رحلة من عدم الفهم التام إلى الاختيار الواثق للصيغة الصحيحة. ستتعلم كيف يقوم TypeScript بترجمة الكود الخاص بك، ولماذا لا يزال require مستخدمًا حتى اليوم، ومتى يجب استخدام import.
1. السياق التاريخي: من أين أتى النظامان
\n\nلفهم الفرق، يجب أن نلقي نظرة على تاريخ JavaScript. في البداية، لم تكن Node.js تحتوي على نظام وحدات مدمج. ابتكر المطورون CommonJS — وهي طريقة متزامنة (synchronous) لاستيراد الوحدات باستخدام require(). هذا الأسلوب يعمل بشكل جيد على الخادم (server-side)، لكنه لم يكن مناسبًا للمتصفحات التي تحتاج إلى تحميل غير متزامن (asynchronous).
لاحقًا، ظهر معيار ES Modules (ESM) — وهو صيغة الوحدات الرسمية لـ ECMAScript. TypeScript، باعتبارها لغة مبنية فوق JavaScript، تدعم كلا الخيارين. ولكن هنا يبدأ الارتباك: تكتب import، والمترجم (compiler) يحوله إلى require — أو العكس.
2. الصيغة (Syntax): الفروق الرئيسية
\n\nدعنا نوضح الفروق بين الأسلوبين.
\n\nCommonJS (require)
\n\n// استيراد الوحدة بأكملها\nconst fs = require('fs');\n\n// استيراد جزء من الوحدة (تفكيك)\nconst { readFile, writeFile } = require('fs');\n\n// التصدير\nmodule.exports = { myFunction, myVariable };\nexports.myFunction = () => { ... };\n\nES Modules (import)
\n\n// استيراد الوحدة بأكملها\nimport fs from 'fs';\n\n// استيراد الصادرات المسماة\nimport { readFile, writeFile } from 'fs';\n\n// استيراد الكل ككائن\nimport * as fs from 'fs';\n\n// التصدير\nexport const myVariable = 42;\nexport function myFunction() { ... }\nexport default class MyClass { }\n\nللوهلة الأولى، الفرق يبدو فقط في الصيغة. لكن دعنا نتعمق أكثر.
\n\n3. الاستيراد الثابت (Static) مقابل الاستيراد الديناميكي (Dynamic)
\n\nهذا هو الفرق الجوهري الذي يؤثر على طريقة عمل الكود الخاص بك.
\n\n- \n
require— ديناميكي. يمكنك استدعاؤه في أي مكان، داخل شرط أو دالة. يتم تحميل الوحدة في وقت الاستدعاء. \n import— ثابت. يتم حل (resolve) عمليات الاستيراد في مرحلة الترجمة (compile time)، قبل تنفيذ الكود. يجب أن تكون دائمًا في المستوى الأعلى من الملف. \n
مثال على require الديناميكي:
\n\nlet myModule;\nif (process.env.NODE_ENV === 'production') {\n myModule = require('./production-config');\n} else {\n myModule = require('./dev-config');\n}\n\nهذه الحيلة لا تعمل مع import — ستظهر أخطاء نحوية. ولكن لهذا الغرض يوجد import() الديناميكي (الذي يُرجع Promise):
if (condition) {\n const module = await import('./some-module');\n module.doSomething();\n}\n\n4. كيف يقوم TypeScript بترجمة الوحدات
\n\nكل شيء يعتمد على إعداد module في ملف tsconfig.json. إليك الخيارات الرئيسية:
- \n
"module": "commonjs"— سيتم تحويلimportالخاص بك إلىrequire. سيعمل الكود في Node.js دون أي إعدادات إضافية. \n "module": "esnext"— سيبقىimportكما هو. مناسب للمتصفحات الحديثة أو أدوات التجميع (مثل Webpack و Vite). \n "module": "node16"أو"nodenext"— سيقوم TypeScript بتحديد النظام المناسب تلقائيًا. \n