> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# JavaScript لمن يعرف لغة أخرى: Node.js مع TypeScript والأدوات
- URL: https://arabroot.io/articles/javascript-لمن-يعرف-لغة-اخرى-node-js-مع-typescript/
- Published: 2026-10-31T06:46:49.000Z
- Updated: 2026-10-31T07:46:54.000Z
- Description: إذا كنت تبرمج بلغة أخرى وانتقلت إلى فريق يعمل بـ Node.js وTypeScript، فهذا يومك الأول: الإصدار الصحيح، والصيغة مقارنة بما تعرفه، وأدوات العمل وهيكل المشروع بأوامر ومخرجات حقيقية.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, لغات البرمجة عملياً

لنفرض أنك مطور تكتب Java أو C# أو Python منذ سنوات، وانتقلت هذا الأسبوع إلى شركة ناشئة تبيع القهوة والتمور والهيل عبر الإنترنت، فتجد أن كل شيء فيها مكتوب بلغة واحدة: الواجهة في المتصفح، وخدمة الطلبات على السيرفر، والسكربتات الصغيرة التي ترسل التقارير، كلها JavaScript وفوقها TypeScript، وتعمل على Node.js. وفي اليوم الأول يطلب منك قائد الفريق مهمة صغيرة: أداة سطر أوامر تحسب سعر السلة بالهللات مع ضريبة القيمة المضافة 15%، لها اختبار، وتمر على فحص الأنواع والتنسيق قبل الدمج. والمهمة نفسها سهلة على أي مبرمج، ولكن الأسئلة التي تقف أمامك ليست عن المنطق، وإنما عن البيئة: أي إصدار من Node.js أثبت؟ وهل أحتاج إلى بناء Build قبل التشغيل كما في Java؟ وما الفرق بين `npm install` و`npm ci`؟ ولماذا يرفض Node.js سطر `import` يعمل في المحرر بلا مشكلة؟ وأين يكتب الاختبار، ومن يقرر أن المسافة البادئة مسافتان وليست Tab؟

وهذا المقال هو أول مقالات مرحلة [JavaScript وTypeScript](https://arabroot.io/start/#stage-lang-js) في مسار لغات البرمجة عملياً، وهو موجه لمن يعرف البرمجة بلغة أخرى ويريد أن يبدأ بشكل صحيح في هذه اللغة، أو يعود إليه كمرجع عندما ينشئ مشروعاً جديداً. فلن نشرح ما هو المتغير أو الحلقة، فهذا موضوع مقال [أساسيات البرمجة](https://arabroot.io/articles/%D8%A7%D8%B3%D8%A7%D8%B3%D9%8A%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A7%D9%84%D9%85%D8%AA%D8%BA%D9%8A%D8%B1%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%B4%D8%B1%D9%88%D8%B7-%D9%88%D8%A7%D9%84%D8%AD%D9%84%D9%82%D8%A7%D8%AA-%D9%88%D8%A7%D9%84%D8%AF%D9%88%D8%A7%D9%84/)، وإنما سوف نبني أداة الهللات خطوة خطوة داخل Container من Docker Image الرسمي لـ Node.js 26، وننقل كل أمر ومخرجه كما ظهر، ومعه الأخطاء التي واجهناها في الطريق.

وسوف نناقش في هذا المقال ما يلي:

- أين تعمل JavaScript اليوم، وما الذي يوجد تحت الأمر `node`: المحرك V8 والمكتبة libuv والمعيار ECMAScript.
- أي إصدار تثبت، وكيف تقرأ جدول إصدارات Node.js، ولماذا الإصدار الزوجي هو إصدار الإنتاج.
- جولة سريعة في الصيغة مقارنة بما تعرفه من Java وPython وC#، مع ملف شغلناه فعلاً.
- كيف يشغل Node.js ملفات TypeScript مباشرة بحذف الأنواع، وما الذي يبقى لمترجم `tsc`.
- سلسلة الأدوات: `npm` وملف القفل، و`tsc` للفحص، وBiome للتنسيق والتدقيق، و`node --test` للاختبارات.
- تجربة عملية كاملة من `npm init` حتى الاختبارات، مع مشكلتين واجهناهما، وتجربة «ماذا يحدث إذا».
- هيكل المشروع، والوحدات ESM وCommonJS، وزمن البدء مع حذف الأنواع وبدونه، ثم الأخطاء الشائعة وقائمة تحقق.

## أين تعمل JavaScript اليوم؟ المتصفح Browser والسيرفر Server

بدأت JavaScript في 1995 لغة صغيرة داخل متصفح Netscape، كتبها Brendan Eich في أيام قليلة لتحريك عناصر الصفحة، وقد حكينا قصتها وقصص لغات أخرى في مقال [قصص لغات البرمجة ومن صنعها](https://arabroot.io/articles/%D9%82%D8%B5%D8%B5-%D9%84%D8%BA%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%88%D9%85%D9%86-%D8%B5%D9%86%D8%B9%D9%87%D8%A7/). وبعدها بسنتين صار للغة معيار رسمي اسمه ECMAScript تصدره منظمة Ecma، وتطوره لجنة اسمها TC39 فيها ممثلون عن صناع المتصفحات والشركات الكبيرة، ومنذ 2015 يصدر المعيار كل سنة بإصدار جديد صغير بدلاً من إصدارات ضخمة متباعدة، وآخرها حتى كتابة هذا المقال هو [الإصدار السابع عشر ECMAScript 2026](https://www.ecma-international.org/publications-and-standards/standards/ecma-262/?ref=arabroot.io). وكل ميزة جديدة تمر بمراحل من 0 إلى 4 في [قائمة مقترحات TC39](https://github.com/tc39/proposals?ref=arabroot.io)، ولا تدخل المعيار إلا بعد أن تطبقها محركات حقيقية، ولذلك فإن السؤال «هل هذه الميزة متاحة؟» يجاب عنه بإصدار المحرك الذي تستخدمه، وليس بإصدار اللغة فقط. وشرحنا الفرق بين المعيار والتطبيق والحوكمة في مقال [مواصفات لغات البرمجة وتطبيقاتها وحوكمتها](https://arabroot.io/articles/%D9%85%D9%88%D8%A7%D8%B5%D9%81%D8%A7%D8%AA-%D9%84%D8%BA%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%88%D8%AA%D8%B7%D8%A8%D9%8A%D9%82%D8%A7%D8%AA%D9%87%D8%A7-%D9%88%D8%AD%D9%88%D9%83%D9%85%D8%AA%D9%87%D8%A7/).

وفي 2009 أخذ Ryan Dahl محرك V8 من متصفح Chrome، ووضعه خارج المتصفح، وأضاف إليه مكتبة للإدخال والإخراج غير المتزامن صارت لاحقاً libuv، فولد Node.js، وصار بالإمكان كتابة سيرفر بنفس اللغة التي تكتب بها الواجهة. والفكرة التي جعلت Node.js ينتشر في السيرفرات ليست اللغة نفسها، وإنما نموذج التنفيذ: خيط واحد Thread ينفذ كودك، وحلقة أحداث Event Loop تستقبل نتائج الشبكة والملفات عندما تجهز، فلا يحجز كل اتصال خيطاً كاملاً ينتظر. وهذا النموذج شرحناه بالتفصيل وقسناه في مقال [حلقة الأحداث Event Loop](https://arabroot.io/articles/%D8%AD%D9%84%D9%82%D8%A9-%D8%A7%D9%84%D8%A7%D8%AD%D8%AF%D8%A7%D8%AB-event-loop-%D9%81%D9%8A-javascript/)، وسوف نعود إليه في مقال التزامن من هذه المرحلة.

والصورة التالية تبين ما يوجد تحت الأمر `node` عندما تشغله، بالإصدارات التي طبعها في التجربة:

![ثلاثة أعمدة: كودك بملفات js وts، ثم Node.js v26.13.0 وفيه المحرك V8 14.6 ومكتبة libuv للحلقة والشبكة والملفات والمكتبات القياسية، ثم نظام التشغيل باستدعاءات مثل epoll، وتحتها npm 11.20.0 والمتصفح الذي فيه محرك مثل V8 مع DOM، والمعيار ECMAScript](https://arabroot.io/content/images/2026/10/js-for-devs-01-stack.webp)

نفس اللغة في المتصفح وفي Node.js، والفرق في المكتبات حول المحرك

والنقطة التي يجب أن تأخذها من الصورة أن «JavaScript» في المتصفح و«JavaScript» في Node.js هي نفس اللغة بنفس المعيار، ولكن ما حولها مختلف تماماً: في المتصفح لديك DOM والنوافذ وواجهات الصفحة، ولا يوجد نظام ملفات، وفي Node.js لديك `node:fs` و`node:http` و`node:crypto` ولا توجد صفحة. ولذلك فإن مكتبة تستخدم `document` لن تعمل في السيرفر، ومكتبة تستخدم `fs` لن تعمل في المتصفح، وهذا يفسر كثيراً من رسائل الخطأ التي تظهر للقادمين الجدد. وتوجد بيئات تشغيل أخرى خارج المتصفح مثل Deno وBun، وكلها تشغل نفس اللغة بمحركات وأدوات مختلفة، ولكن Node.js هو الأكثر استخداماً في الشركات، وهو ما سوف نستخدمه في كل مقالات هذه المرحلة.

والمحرك V8 لا يفسر الكود سطراً سطراً بالطريقة القديمة، وإنما يحوله إلى Bytecode ينفذه مفسر سريع، ثم يرصد الدوال التي تتكرر كثيراً فيترجمها إلى كود الآلة أثناء التشغيل Just-In-Time، وهذا ما يجعل JavaScript أسرع بكثير مما يتوقع من لغة ديناميكية. وإذا كنت قادماً من Java أو C# فهذه نفس فكرة JVM وCLR تقريباً، والفرق أنه لا توجد خطوة ترجمة منفصلة يراها المطور، فأنت تعطي Node.js الملف المصدري مباشرة. وقد شرحنا المترجم والمفسر والـ JIT بالتفصيل في مقال [المترجم والمفسر وJIT وWebAssembly](https://arabroot.io/articles/%D8%A7%D9%84%D9%85%D8%AA%D8%B1%D8%AC%D9%85-%D9%88%D8%A7%D9%84%D9%85%D9%81%D8%B3%D8%B1-%D9%88-jit-%D9%88-webassembly-%D8%A8%D8%A7%D9%84%D8%AA%D9%81%D8%B5%D9%8A%D9%84/).

## أي إصدار تثبت؟ جدول إصدارات Node.js Release Schedule

أول قرار يواجهك هو الإصدار، وهو قرار يتكرر كل سنة، لذلك من المفيد أن تفهم القاعدة بدلاً من أن تحفظ رقماً. فمشروع Node.js ينشر جدولاً رسمياً في [مستودع فريق الإصدارات](https://github.com/nodejs/Release?ref=arabroot.io)، وفي صفحة [الإصدارات السابقة](https://nodejs.org/en/about/previous-releases?ref=arabroot.io) على الموقع، والقاعدة فيه أن كل إصدار رئيسي يمر بثلاث مراحل:

- الإصدار الحالي Current: يبدأ في الربيع، ويستقبل الميزات الجديدة بسرعة، وهو للتجربة ولمطوري المكتبات.
- الدعم طويل المدى النشط Active LTS: في أكتوبر يدخل الإصدار الزوجي هذه المرحلة، فيأخذ اسماً رمزياً، ويستقبل الإصلاحات والتحسينات الآمنة فقط، وهذا هو إصدار الإنتاج.
- دعم الصيانة Maintenance LTS: بعد سنة تقريباً ينتقل إلى إصلاحات الأمان والأخطاء الحرجة فقط، ثم ينتهي دعمه بعد 30 شهراً من بدايته تقريباً.

وبحسب الجدول الرسمي وقت كتابة هذا المقال، فإن Node.js 26 بالاسم الرمزي Lithium دخل مرحلة Active LTS في 28 أكتوبر 2026 وينتهي دعمه في أبريل 2029، وNode.js 24 بالاسم Krypton انتقل إلى الصيانة في 20 أكتوبر 2026، وNode.js 22 بالاسم Jod في الصيانة حتى أبريل 2027\. وفي نفس الجدول ظهر Node.js 27 كإصدار Alpha، لأن المشروع بدأ يجرب نموذجاً جديداً للإصدارات. والقاعدة العملية لمشروع جديد بسيطة: ابدأ بآخر إصدار في Active LTS، أي Node.js 26 الآن، ولا تضع الإصدارات الفردية مثل 25 في الإنتاج، والسبب أن الإصدار الفردي لا يدخل LTS أبداً، وينتهي دعمه بعد أشهر قليلة، فتجد نفسك مضطراً للترقية في وقت لم تختره.

وقد يتساءل البعض: لماذا لا أثبت Node.js من مستودع التوزيعة نفسها بالأمر `apt install nodejs`؟ والإجابة أن هذا هو الطريق الأسهل والأسوأ في نفس الوقت، فالتوزيعات المستقرة تجمد الإصدار عند بداية دورتها، فقد تجد على السيرفر إصداراً انتهى دعمه أو قارب، ومعه npm قديم. والطريق الثاني الذي يقترحه كثير من الشروحات هو تحميل سكربت من الإنترنت وتمريره مباشرة إلى `bash`، وهو يعطيك إصداراً حديثاً، ولكنه يشغل كوداً لم تقرأه بصلاحيات root. والطريقة الأنسب تختلف بحسب المكان:

- جهاز المطور: مدير إصدارات مثل [nvm](https://github.com/nvm-sh/nvm?ref=arabroot.io) أو fnm، والسبب أنك ستعمل على أكثر من مشروع، وكل مشروع قد يحتاج إصداراً مختلفاً، ومدير الإصدارات يقرأ الإصدار من ملف `.nvmrc` في المشروع ويبدل بينها بأمر واحد.
- جهاز Windows أو macOS لمن يريد إصداراً واحداً: المثبت الرسمي من [صفحة التحميل](https://nodejs.org/en/download?ref=arabroot.io).
- السيرفر والـ CI والتجارب: [Docker Image الرسمي](https://hub.docker.com/%5F/node?ref=arabroot.io) بإصدار مثبت بالكامل مثل `node:26.13.0-slim`، وليس `node:latest`، والسبب أن `latest` يتغير تحت قدميك ويأخذ الإصدار الفردي أيضاً.

وفي هذا المقال استخدمنا الطريقة الثالثة، فكل الأوامر عملت داخل Container من `node:26.13.0-slim` المبني على Debian 13، وأول ما طبعناه هو الإصدارات:

```bash
node --version
npm --version
node -p "process.versions.v8"
node -p "process.features.typescript"
```

والمخرج سوف يكون كما يلي:

```text
v26.13.0
11.20.0
14.6.202.34-node.40
strip
```

في المخرج أعلاه لاحظ التالي:

- السطر الأول هو إصدار Node.js، والثاني إصدار npm الذي يأتي معه، فلا تثبت npm منفصلاً إلا إذا احتجت إصداراً أحدث لسبب واضح.
- السطر الثالث إصدار المحرك V8، وهو الذي يحدد ميزات اللغة المتاحة، فميزة دخلت المعيار حديثاً تعمل إذا كان المحرك يطبقها.
- السطر الرابع يقول إن هذا الإصدار يدعم TypeScript بطريقة `strip`، أي حذف الأنواع، وسوف نعود إلى معناه بعد قليل.

## جولة سريعة في الصيغة Syntax لمن يعرف لغة أخرى

أغلب صيغة JavaScript مألوفة لمن يكتب Java أو C# أو C، فالأقواس المعقوفة والفاصلة المنقوطة و`if` و`for` و`while` و`switch` و`try` كلها كما تعرفها. والمشكلة ليست في ما يشبه لغتك، وإنما في ما يشبهها شكلاً ويختلف عنها سلوكاً، لذلك كتبنا ملفاً واحداً اسمه `tour.ts` يمر على الأشياء التي يسأل عنها القادمون من لغات أخرى، وشغلناه مباشرة بـ Node.js:

```typescript
// A quick tour for developers coming from another language.  node src/tour.ts
// 1. Variables: const by default, let when you reassign, never var
const vatPercent = 15;
let count = 0;
count += 1;

// 2. Equality: === compares without converting types
console.log('1. ==  vs ===  :', '15' == 15 as unknown, '15' === (15 as unknown));

// 3. Functions: declarations, arrows, default and rest parameters
function addVat(halalas: number, percent = vatPercent): number {
  return halalas + Math.floor((halalas * percent + 50) / 100);
}
const sum = (...xs: number[]) => xs.reduce((a, b) => a + b, 0);
console.log('2. functions   :', addVat(2970), sum(990, 990, 990));

// 4. Objects and arrays, destructuring and spread
const product = { id: 3, nameAr: 'هيل حب 100 غرام', priceHalalas: 990, stock: 10 };
const { nameAr, ...rest } = product;
const restocked = { ...product, stock: product.stock + 5 };
console.log('3. objects     :', nameAr, rest, restocked.stock, product.stock);
const prices = [4500, 3800, 990];
console.log('4. arrays      :', prices.map(p => p / 100), prices.filter(p => p > 1000).length, prices.includes(990));

// 5. Classes with private fields (#) and a type for the shape
type Line = { productId: number; qty: number };
class Cart {
  #lines: Line[] = [];
  add(productId: number, qty = 1): this { this.#lines.push({ productId, qty }); return this; }
  get size(): number { return this.#lines.reduce((n, l) => n + l.qty, 0); }
}
console.log('5. class       :', new Cart().add(3, 2).add(1).size);

// 6. null and undefined, optional chaining and nullish coalescing
const order: { customer?: { phone?: string } } = {};
console.log('6. ?. and ??   :', order.customer?.phone ?? 'no phone', 0 || 'fallback', 0 ?? 'fallback');

// 7. Template literals and string methods
console.log(`7. template    : ${count} item, ${(addVat(990) / 100).toFixed(2)} SAR`.padEnd(10));

// 8. Async: every I/O function returns a Promise, you wait for it with await
const wait = (ms: number) => new Promise<string>(r => setTimeout(() => r(`waited ${ms} ms`), ms));
console.log('8. await       :', await wait(20));

// 9. Errors are thrown and caught like Java or C#
try { JSON.parse('{bad'); } catch (e) { console.log('9. try/catch   :', (e as Error).name); }
```

والمخرج سوف يكون كما يلي:

```text
1. ==  vs ===  : true false
2. functions   : 3416 2970
3. objects     : هيل حب 100 غرام { id: 3, priceHalalas: 990, stock: 10 } 15 10
4. arrays      : [ 45, 38, 9.9 ] 2 true
5. class       : 3
6. ?. and ??   : no phone fallback 0
7. template    : 1 item, 11.39 SAR
8. await       : waited 20 ms
9. try/catch   : SyntaxError
```

وهذا المخرج يحتاج قراءة سطراً سطراً، لأن كل سطر فيه درس:

- السطر 1: `'15' == 15` أعطى `true`، لأن `==` يحول النص إلى رقم قبل المقارنة، و`===` أعطى `false` لأنه يقارن النوع والقيمة معاً. والقاعدة التي تتبعها كل الفرق تقريباً: استخدم `===` دائماً، وسوف ترى بعد قليل أن أداة التدقيق تمنع `==` تلقائياً. ولاحظ أننا احتجنا `as unknown` حتى يقبل TypeScript المقارنة أصلاً، فهو يعرف أن مقارنة نص برقم خطأ في أغلب الحالات.
- السطر 2: الدالة `addVat` أخذت المعامل الثاني من القيمة الافتراضية، فأعطت 3416 هللة لسلة قيمتها 2970، أي 29.70 ريالاً مع ضريبة 4.46\. ولاحظ أننا نحسب بالهللات كأعداد صحيحة، والسبب سوف تراه في مقال الأنواع من هذه المرحلة عندما نضرب 9.90 في 3.
- السطر 3: الـ Destructuring أخرج `nameAr` وجمع الباقي في `rest`، والـ Spread أنشأ كائناً جديداً بمخزون 15، وبقي الأصل 10\. وهذه النسخة سطحية Shallow Copy، فإذا كان في الكائن مصفوفة فالنسختان تشيران إلى نفس المصفوفة، وهذا من أكثر الأخطاء التي سوف نقيسها في مقال الأخطاء الشائعة.
- السطر 4: `map` و`filter` و`includes` تعمل كما في Streams في Java أو LINQ في C#، ولكنها ليست كسولة Lazy، فكل خطوة تنشئ مصفوفة جديدة كاملة.
- السطر 5: الحقل `#lines` خاص فعلاً وقت التشغيل، فلا يمكن الوصول إليه من خارج الكلاس حتى من JavaScript عادية، وهذا يختلف عن الكلمة `private` في TypeScript التي تحذف مع الأنواع ولا تمنع شيئاً وقت التشغيل.
- السطر 6: هنا الفرق الذي يربك القادمين من Python: `0 || 'fallback'` أعطى `fallback` لأن `||` يعامل الصفر كقيمة «خاطئة» Falsy، أما `0 ?? 'fallback'` فأعطى 0 لأن `??` لا يستبدل إلا `null` و`undefined`. وإذا كان المخزون أو الخصم قد يكون صفراً فاستخدم `??` دائماً.
- السطر 8: `await` في أعلى الملف تعمل مباشرة، لأن الملف وحدة ES Module كما سوف نرى، وهذه الميزة اسمها Top-level await.

والصورة التالية تجمع هذه المقارنات في جدول واحد يمكنك الرجوع إليه:

![جدول من ثمانية صفوف يقارن JavaScript وTypeScript مع Python وJava وC#: المتغير بـ const وlet، والمساواة بثلاث علامات، والدالة السهمية، وnull وundefined، والكائن البسيط، والكلاس بحقل خاص، والوحدات بامتداد إجباري، وانتظار I/O بـ await، مع عمود لما يجب الانتباه له](https://arabroot.io/content/images/2026/10/js-for-devs-02-syntax.webp)

ما يشبه لغتك شكلاً ويختلف عنها سلوكاً

وهناك ثلاثة أشياء في JavaScript لا يوجد ما يقابلها تماماً في أغلب اللغات، ويستحق كل واحد منها انتباهك من اليوم الأول. الأول هو `this`، فقيمته لا تحدد بمكان كتابة الدالة، وإنما بطريقة استدعائها، فإذا مررت دالة من كلاس كمعامل إلى دالة أخرى فقد تفقد `this`، والدوال السهمية Arrow Functions تحل ذلك لأنها تأخذ `this` من المكان الذي كتبت فيه. والثاني أن الأرقام كلها من نوع واحد هو `number` بدقة 64 بت عشرية، فلا يوجد `int` منفصل، والثالث أن كل عملية إدخال وإخراج ترجع وعداً Promise ولا توقف الخيط، وهذا عكس ما تعودت عليه في Java أو Python التقليدية حيث ينتظر الخيط حتى تصل النتيجة. والثلاثة لها مقالات كاملة في هذه المرحلة، فلا تحاول حفظ قواعدها الآن، ولكن تذكر أنها موجودة عندما ترى نتيجة غريبة.

## TypeScript فوق JavaScript: ماذا يفعل Node.js بالأنواع؟ Type Stripping

لاحظت أننا شغلنا ملف `.ts` بالأمر `node` مباشرة، دون أي خطوة بناء، وهذا جديد نسبياً. فقبل 2025 كان تشغيل TypeScript يحتاج إما أن تترجم الكود بـ `tsc` إلى مجلد JavaScript ثم تشغله، أو أن تستخدم أداة وسيطة مثل ts-node أو tsx تترجم في الذاكرة. أما الآن فإن Node.js نفسه يقرأ ملف TypeScript، ويحذف منه كل ما هو نوع، ويشغل ما تبقى، وهذه الميزة اسمها حذف الأنواع Type Stripping. وبحسب [صفحة TypeScript في توثيق Node.js 26](https://nodejs.org/docs/latest-v26.x/api/typescript.html?ref=arabroot.io) فقد أضيفت الميزة في الإصدار 22.6 خلف خيار، ثم صارت مفعلة افتراضياً في 23.6، ثم مستقرة Stable في 25.2 و24.12، وفي الإصدار 26 حذف الخيار القديم `--experimental-transform-types` نهائياً.

وكلمة «حذف» هنا دقيقة، فـ Node.js لا يفحص الأنواع إطلاقاً، وإنما يستبدل كل نوع بمسافات ويترك الباقي كما هو، ولذلك يعمل الكود الذي فيه خطأ أنواع وكأن شيئاً لم يكن، وسوف ترى ذلك بعينك في التجربة. وهذا يعني أن العمل صار مقسماً على أداتين: Node.js يشغل، والمترجم `tsc` يفحص. والحذف بهذه الطريقة يعمل فقط مع الصيغ التي يمكن حذفها دون أن تولد كوداً، فـ `enum` وخصائص المنشئ Parameter Properties و`namespace` التي فيها كود لا تعمل، لأن كل واحدة منها تحتاج أن يولد المترجم كود JavaScript جديداً. وقد شرحنا هذا كله بالتفصيل مع تجربة الأخطاء التسعة في مقال [TypeScript للمطورين: أي الأخطاء تكشفها الأنواع؟](https://arabroot.io/articles/typescript-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%85%D8%A7-%D8%A7%D9%84%D8%B0%D9%8A-%D8%AA%D9%83%D8%B4%D9%81%D9%87-%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9/)، فلن نكرره هنا، ويكفي أن تعرف أن الخيار `erasableSyntaxOnly` في `tsconfig.json` يجعل `tsc` يرفض هذه الصيغ من البداية، فلا تكتشفها وقت التشغيل.

وقد يتساءل البعض: إذا كان Node.js يشغل TypeScript، فما الحاجة إلى `tsc` أصلاً؟ والإجابة أن الفائدة كلها من TypeScript هي الفحص، والحذف وحده يعطيك ملفاً يعمل دون أي ضمان، فهو أشبه بأن تكتب الأنواع كتعليقات. لذلك فإن الشكل الذي تعمل به أغلب الفرق اليوم هو: المحرر يفحص أثناء الكتابة، و`tsc --noEmit` يفحص في CI ويمنع الدمج عند الخطأ، وNode.js يشغل الملفات مباشرة في التطوير، وأحياناً في الإنتاج أيضاً كما سوف نناقش عند الكلام عن زمن البدء.

## أدوات العمل Toolchain: التشغيل والحزم والفحص والتنسيق

في Java تعرف أن JDK يعطيك المترجم `javac` وأن Maven أو Gradle يبني المشروع ويثبت المكتبات، وفي Python تعرف `pip` وبيئة `venv`. وفي عالم Node.js توجد أدوات كثيرة لكل خطوة، وهذا من أكثر ما يربك القادمين، لأن كل مقال تقرؤه يستخدم مجموعة مختلفة. لذلك سوف نحدد لكل خطوة أداة واحدة معقولة لمشروع جديد، ونذكر البدائل في جملة:

- التشغيل Runtime: `node` نفسه، وهو أيضاً الذي يحذف الأنواع كما رأينا.
- مدير الحزم Package Manager: `npm` الذي يأتي مع Node.js، والبدائل المعروفة pnpm وYarn، وقد قارنا مديري الحزم في اللغات المختلفة في مقال [مديرو الحزم npm وpip وMaven وCargo](https://arabroot.io/articles/%D9%85%D8%AF%D9%8A%D8%B1%D9%88-%D8%A7%D9%84%D8%AD%D8%B2%D9%85-npm-%D9%88-pip-%D9%88-maven-%D9%88-cargo/).
- فحص الأنواع Type Checker: `tsc` من الحزمة `typescript`، والإصدار 7 منها مترجم أصلي مكتوب بلغة Go وأسرع بكثير من الإصدارات السابقة.
- التنسيق Formatter والتدقيق Linter: [Biome](https://biomejs.dev/?ref=arabroot.io)، وهو أداة واحدة تقوم بعمل Prettier وESLint معاً، وأسرع منهما لأنها مكتوبة بـ Rust. والبديل التقليدي هو ESLint مع Prettier، وهو ما سوف تجده في أغلب المشاريع القديمة، وBiome يوفر [دليل انتقال](https://biomejs.dev/guides/migrate-eslint-prettier/?ref=arabroot.io) منهما.
- الاختبارات Test Runner: المشغل المدمج [node:test](https://nodejs.org/docs/latest-v26.x/api/test.html?ref=arabroot.io)، والبدائل Vitest وJest، وسوف نستخدم المدمج في هذه المرحلة لأنه لا يحتاج أي حزمة.

والصورة التالية تبين هذه الأدوات بالإصدارات التي استخدمناها، وما يفعله كل منها:

![أربعة صناديق: التشغيل بـ node v26.13.0 الذي يحذف الأنواع ولا يفحصها، والحزم بـ npm 11.20.0 مع package.json وpackage-lock.json، والفحص بـ tsc من TypeScript 7.0.2، والتنسيق والتدقيق بـ Biome 2.6.4، وتحتها الاختبارات بـ node --test، وما لا يحتاجه مشروع سيرفر جديد مثل Babel وts-node وwebpack، وسطر يقول إن التثبيت كان 6 حزم بحجم 119 MB](https://arabroot.io/content/images/2026/10/js-for-devs-03-toolchain.webp)

أداة واحدة لكل خطوة، وكلها تعمل من سكربتات package.json

ولاحظ أن القائمة لا تحتوي على Babel ولا webpack ولا ts-node، وهذه أدوات ستجدها في أغلب الشروحات القديمة، وكانت ضرورية عندما كان Node.js لا يفهم `import` ولا TypeScript. أما في مشروع سيرفر جديد على Node.js 26 فلا تحتاج أياً منها، وكل أداة إضافية هي إعداد آخر يجب أن تفهمه وتحدثه. وأدوات التجميع Bundlers مثل Vite وesbuild مكانها الواجهات في المتصفح، وقد شرحنا أدوات البناء بشكل عام في مقال [أدوات البناء Build Tools](https://arabroot.io/articles/%D8%A7%D8%AF%D9%88%D8%A7%D8%AA-%D8%A7%D9%84%D8%A8%D9%86%D8%A7%D8%A1-build-tools-%D9%85%D9%86-make-%D8%A7%D9%84%D9%89-maven-%D9%88-gradle-%D9%88-dotnet/).

## التجربة العملية Lab: أداة الهللات من الصفر

الآن سوف نبني المهمة التي طلبها قائد الفريق: مشروع صغير اسمه `halala-cli` يحسب سعر السلة بالهللات مع الضريبة، فيه دالة الحساب، وملف التشغيل، واختبار، ويمر على `tsc` وBiome. وكل الأوامر التالية عملت داخل Container مؤقت، والملفات كاملة موجودة في مجلد `lab` مع المقال، والسكربت الذي جمعها اسمه `run.sh`، ويمكنك تشغيله على جهازك بالأمر:

```bash
docker run --rm -v "$PWD:/lab:ro" node:26.13.0-slim sh /lab/run.sh
```

### الخطوة الأولى: package.json وملف المصدر

كل مشروع Node.js يبدأ بملف `package.json`، وهو يقابل `pom.xml` في Maven أو `pyproject.toml` في Python: اسم المشروع، وإصداره، وسكربتاته، والحزم التي يعتمد عليها. وننشئه بالأمر `npm init`، ثم نضيف الحقل `type` بقيمة `module`:

```bash
npm init -y
npm pkg set type=module name=halala-cli
cat package.json
```

والمخرج سوف يكون كما يلي:

```json
{
  "name": "halala-cli",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "module"
}
```

والحقل `"type": "module"` هو أهم سطر في الملف، لأنه يقول لـ Node.js إن ملفات `.js` و`.ts` في هذا المشروع وحدات ES Modules تستخدم `import` و`export`، وسوف ترى في تجربة «ماذا يحدث إذا» ما يحدث عندما تحذفه. والحقل `main` وسكربت `test` الافتراضي لا يفيداننا، وسوف نستبدلهما بعد قليل.

بعد ذلك نكتب دالة الحساب في `src/price.ts`، والقاعدة الأولى فيها أن المبالغ أعداد صحيحة بالهللات وليست كسوراً بالريال:

```typescript
// Prices are integers in halalas (1 SAR = 100 halalas), never floats.
export type Item = { name: string; priceHalalas: number; qty: number };

export const VAT_PERCENT = 15;

export function subtotal(items: Item[]): number {
  return items.reduce((sum, it) => sum + it.priceHalalas * it.qty, 0);
}

// VAT rounded half up, in integer arithmetic only
export function vat(halalas: number): number {
  return Math.floor((halalas * VAT_PERCENT + 50) / 100);
}

export function formatSar(halalas: number): string {
  return `${(halalas / 100).toFixed(2)} ر.س`;
}
```

والدالة `vat` تقرب النصف إلى الأعلى Round Half Up بعملية صحيحة: نضرب في 15، ونضيف 50، ثم نقسم على 100 ونأخذ الجزء الصحيح. فسلة بـ 2970 هللة ضريبتها 44550 على 100، أي 445.5 هللة، وبإضافة 50 قبل القسمة تصبح 446\. وهذا الحساب نفسه هو الذي سوف نستخدمه في خدمة الطلبات في مقال الخدمة الحقيقية، وفي كل لغات المسار، حتى تكون المقارنة عادلة. ثم ملف التشغيل `src/cli.ts`:

```typescript
import { subtotal, vat, formatSar, type Item } from './price.ts';

const cart: Item[] = [
  { name: 'هيل حب 100 غرام', priceHalalas: 990, qty: 3 },
  { name: 'تمر سكري 1 كيلو', priceHalalas: 3800, qty: 1 },
];

const sub = subtotal(cart);
const tax = vat(sub);
for (const { name, priceHalalas, qty } of cart) {
  console.log(`${qty} x ${name} @ ${formatSar(priceHalalas)}`);
}
console.log(`subtotal ${formatSar(sub)} | VAT ${formatSar(tax)} | total ${formatSar(sub + tax)}`);
```

ولاحظ في سطر `import` شيئين: الامتداد `.ts` مكتوب صراحة، والكلمة `type` قبل `Item` تقول إن هذا الاسم نوع فقط يحذف بالكامل. ونشغل الملف مباشرة قبل تثبيت أي حزمة:

```bash
node src/cli.ts
```

والمخرج سوف يكون كما يلي:

```text
3 x هيل حب 100 غرام @ 9.90 ر.س
1 x تمر سكري 1 كيلو @ 38.00 ر.س
subtotal 67.70 ر.س | VAT 10.16 ر.س | total 77.86 ر.س
```

وهذه أول نقطة تختلف عن Java وC#: لا توجد خطوة ترجمة، ولا توجد حزمة مثبتة، وملف TypeScript عمل بـ Node.js وحده. والمجموع صحيح: 2970 للهيل و3800 للتمر يعطيان 6770 هللة، وضريبتها 1015.5 تقرب إلى 1016.

### الخطوة الثانية: تثبيت الأدوات وملف القفل Lockfile

الآن نثبت أدوات التطوير: TypeScript للفحص، وتعريفات أنواع Node.js، وBiome. والخيار `-D` يضعها في `devDependencies`، أي أنها تلزم للتطوير وليس لتشغيل الأداة:

```bash
npm install -D --no-fund typescript@7 @types/node@26 @biomejs/biome
npm ls --depth=0
du -sh node_modules
```

والمخرج سوف يكون كما يلي:

```text
added 6 packages, and audited 7 packages in 1m

found 0 vulnerabilities
install took 85305 ms
halala-cli@1.0.0 /work
+-- @biomejs/biome@2.6.4
+-- @types/node@26.6.6
`-- typescript@7.0.2

packages in node_modules: 6
119M	node_modules
```

في المخرج أعلاه لاحظ التالي:

- ثلاث حزم طلبناها، وثبت npm ست حزم، لأن كل حزمة تجلب ما تعتمد عليه: Biome يجلب ملفه التنفيذي الخاص بنظام Linux x64 كحزمة منفصلة، و`@types/node` يجلب `undici-types`، وTypeScript 7 يجلب مترجمه الأصلي.
- حجم `node_modules` 119 ميجابايت لثلاث أدوات فقط، وأغلبه ملفات تنفيذية أصلية، وهذا سبب أن المجلد لا يدخل Git أبداً، وأن ترتيب طبقات Dockerfile مهم كما سوف نرى في مقال الخدمة.
- التثبيت أخذ 85 ثانية في المرة الأولى، وأغلب الوقت تحميل من الشبكة داخل Docker Desktop.
- السطر `found 0 vulnerabilities` نتيجة `npm audit` الذي يعمل تلقائياً مع كل تثبيت ويقارن الحزم بقاعدة الثغرات المعروفة.

ومع التثبيت ظهر ملف جديد اسمه `package-lock.json` بحجم 19 كيلوبايت تقريباً، وهو أهم ملف لا يقرؤه أحد. ففي `package.json` كتب npm المدى `^7.0.2`، أي «7.0.2 أو أي إصدار أحدث في السلسلة 7»، أما ملف القفل فيسجل الإصدار الدقيق الذي ثبت لكل حزمة وهاش محتواها، كما يشرح [توثيق package-lock.json](https://docs.npmjs.com/cli/v11/configuring-npm/package-lock-json?ref=arabroot.io). ولذلك يجب أن يدخل Git، وفي CI وفي Docker تستخدم الأمر `npm ci` بدلاً من `npm install`، والسبب كما في [توثيق npm ci](https://docs.npmjs.com/cli/v11/commands/npm-ci?ref=arabroot.io) أنه يحذف `node_modules` ويثبت من ملف القفل حرفياً، ويفشل إذا كان الملفان غير متوافقين بدلاً من أن يعدل ملف القفل بصمت. وعندما جربناه بعد حذف `node_modules` أخذ 13.5 ثانية بدلاً من 85، لأن الحزم صارت في ذاكرة npm المؤقتة Cache.

⚠️

لا تحذف `package-lock.json` لتحل مشكلة تثبيت، فهذا يرفع كل الحزم غير المباشرة إلى آخر إصدار في مداها مرة واحدة، وقد تدخل معها مشكلة لا علاقة لها بما كنت تحاول إصلاحه. حدث الحزم بشكل مقصود بالأمر `npm update` أو `npm install اسم-الحزمة@الإصدار`، وراجع الفرق في ملف القفل قبل الدمج.

وفي مارس 2016 حذف مطور حزمة صغيرة اسمها left-pad من سجل npm بعد خلاف مع الشركة، وكانت الحزمة من 11 سطراً تعتمد عليها مكتبات كبيرة بشكل غير مباشر، فتوقف بناء آلاف المشاريع حول العالم ساعات، وغيرت npm بعدها سياسة حذف الحزم المنشورة كما شرحت في [تدوينتها عن الحادثة](https://blog.npmjs.org/post/141577284765/kik-left-pad-and-npm?ref=arabroot.io). والدرس الذي بقي من القصة أن شجرة الاعتماديات في JavaScript أعمق مما تتوقع، فكل حزمة تضيفها تجلب معها حزماً لم تخترها، ولذلك فإن ملف القفل ومراجعة ما تضيفه ليسا رفاهية.

### الخطوة الثالثة: الفحص بـ tsc وملف tsconfig.json

الآن نكتب `tsconfig.json` الذي يقول لـ `tsc` كيف يفحص، والفكرة فيه أن `tsc` لا يخرج أي ملف، وإنما يفحص فقط، والتشغيل يبقى لـ Node.js:

```json
{
  "compilerOptions": {
    "target": "es2024",
    "module": "nodenext",
    "strict": true,
    "noEmit": true,
    "allowImportingTsExtensions": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "types": ["node"]
  },
  "include": ["src", "test"]
}
```

في الإعداد أعلاه لاحظ التالي:

- `module: nodenext`: يجعل `tsc` يحل الوحدات بنفس قواعد Node.js، فيقرأ `"type"` من `package.json` ويطلب الامتداد في كل `import` نسبي، فيكتشف خطأ الامتداد قبل التشغيل.
- `strict: true`: يفعل مجموعة الفحوص الصارمة، وأهمها أن `null` و`undefined` ليستا من كل الأنواع، وقد صار مفعلاً افتراضياً منذ TypeScript 6.
- `noEmit` و`allowImportingTsExtensions`: الثاني يسمح بكتابة `.ts` في `import`، وهو مسموح فقط عندما لا يخرج المترجم ملفات، لأن الملف الخارج سيكون `.js`.
- `erasableSyntaxOnly` و`verbatimModuleSyntax`: الأول يرفض `enum` وما شابهها مما لا يعمل مع الحذف، والثاني يجبرك على كتابة `import type` أو `type` للأنواع، حتى يعرف الحذف ما يحذفه من سطر `import`.
- `types: ["node"]`: يحمل تعريفات Node.js فقط، فلا تظهر أنواع مكتبات أخرى في المشروع دون أن تطلبها.

ونشغل الفحص على الكود الصحيح أولاً، ثم نزرع خطأ واحداً من النوع الذي يحدث فعلاً: سعر وصل من نموذج أو من ملف CSV كنص `"38.00"` بدلاً من رقم:

```bash
npx tsc -p . && echo "tsc: no errors"
sed 's/priceHalalas: 3800, qty: 1/priceHalalas: "38.00", qty: 1/' src/cli.ts > /tmp/cli.ts && cp /tmp/cli.ts src/cli.ts
node src/cli.ts
npx tsc -p .; echo "tsc exit $?"
```

والمخرج سوف يكون كما يلي:

```text
tsc: no errors

3 x هيل حب 100 غرام @ 9.90 ر.س
1 x تمر سكري 1 كيلو @ 0.38 ر.س
subtotal 30.08 ر.س | VAT 4.51 ر.س | total 34.59 ر.س
src/cli.ts(5,30): error TS2322: Type 'string' is not assignable to type 'number'.
tsc exit 1
```

وهذا المخرج هو أهم ما في المقال كله، فاقرأه بتمهل. فقد عمل Node.js بالملف الخاطئ دون أي اعتراض، لأنه يحذف الأنواع ولا يفحصها، وأعطى فاتورة خاطئة بالكامل: التمر صار بـ 0.38 ريال لأن `formatSar` قسمت النص `"38.00"` على 100 فحولته JavaScript إلى رقم، والمجموع صار 30.08 لأن `"38.00" * 1` أعطى الرقم 38 فجمع مع 2970\. ولم يرم أي سطر استثناء، ولو كانت هذه خدمة حقيقية لحفظ الطلب بهذا المبلغ. أما `tsc` فأمسك الخطأ في السطر 5 العمود 30 بالضبط، وخرج برمز 1، وهذا الرمز هو ما يجعل خطوة CI تفشل وتمنع الدمج. ولهذا السبب فإن `tsc` ليس اختيارياً في مشروع TypeScript، حتى لو كان Node.js يشغل الملفات بدونه.

### الخطوة الرابعة: الاختبارات مع node:test

المشغل المدمج `node:test` يكفي لأغلب المشاريع، وصيغته قريبة مما تعرفه من JUnit أو pytest: دالة `test` باسم وجسم، ومكتبة `assert` للمقارنات. ونكتب في `test/price.test.ts` اختبار الضريبة بالرقم الذي نستخدمه في كل المسار، 9.90 ريال في 3 تساوي 29.70، وضريبتها 4.455 تقرب إلى 4.46، والمجموع 34.16:

```typescript
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { subtotal, vat } from '../src/price.ts';

test('VAT rounds half up: 29.70 SAR -> 4.46', () => {
  const sub = subtotal([{ name: 'x', priceHalalas: 990, qty: 3 }]);
  assert.equal(sub, 2970);
  assert.equal(vat(sub), 446);
  assert.equal(sub + vat(sub), 3416);
});

test('empty cart costs nothing', () => {
  assert.equal(subtotal([]), 0);
  assert.equal(vat(0), 0);
});
```

ونشغل الاختبارات بالأمر `node --test` دون أي معامل:

```text
✔ VAT rounds half up: 29.70 SAR -> 4.46 (1.724708ms)
✔ empty cart costs nothing (0.333075ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1029.912013
```

ولاحظ أننا لم نخبر Node.js أين الاختبارات، فهو يبحث افتراضياً عن ملفات بأسماء مثل `*.test.ts` و`*.test.js` في المشروع، ويشغل كل ملف في عملية Process منفصلة، ولذلك أخذت الجولة كلها ثانية تقريباً بينما الاختباران نفسهما أقل من 2 ميلي ثانية، فأغلب الوقت تشغيل Node.js نفسه وحذف الأنواع. واستخدمنا `node:assert/strict` وليس `node:assert`، والسبب أن النسخة القديمة تقارن بـ `==` في بعض الدوال، فتعتبر `'446'` مساوية لـ `446`. وإذا احتجت لاحقاً ميزات مثل المحاكاة Mocking المتقدمة أو واجهة رسومية للنتائج، فـ Vitest هو الخيار الأقرب، وقد شرحنا أنواع الاختبارات ومتى تستخدم كل نوع في مقال [أنواع الاختبارات](https://arabroot.io/articles/%D8%A7%D9%86%D9%88%D8%A7%D8%B9-%D8%A7%D9%84%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1%D8%A7%D8%AA-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86-unit-%D9%88-integration-%D9%88-e2e/).

### الخطوة الخامسة: التنسيق والتدقيق مع Biome

الآن نجرب Biome على ملف صغير فيه ثلاث مشكلات من النوع الذي يظهر في مراجعة الكود كل يوم: متغير لا يستخدم، و`let` لمتغير لا يتغير، و`==` بدلاً من `===`:

```typescript
export function isFree(price: number) {
  let unused = 1
  if (price == 0) { return true }
  return false
}
```

```bash
npx biome check src/bad.ts
```

والمخرج سوف يكون كما يلي، مختصراً إلى أسطر العناوين:

```text
biome exit 1
src/bad.ts:2:3 lint/style/useConst  FIXABLE
src/bad.ts:2:7 lint/correctness/noUnusedVariables  FIXABLE
src/bad.ts:3:13 lint/suspicious/noDoubleEquals  FIXABLE
src/bad.ts format
Checked 1 file in 10ms. No fixes applied.
Found 2 errors.
Found 2 warnings.
```

وكل سطر هنا قاعدة لها اسم ومجموعة: `useConst` من قواعد الأسلوب، و`noUnusedVariables` من قواعد الصحة، و`noDoubleEquals` من القواعد «المشبوهة» التي تكشف أخطاء محتملة، ثم `format` يعني أن شكل الملف لا يطابق المنسق. وكل قاعدة في المخرج الكامل تأتي مع شرح وسبب واقتراح إصلاح، مثل «المتغير الذي لا يعاد إسناده أوضح كـ const». وعندما نطلب الإصلاح بالأمر `npx biome check --write --unsafe src/bad.ts` يصبح الملف كما يلي:

```typescript
export function isFree(price: number) {
	const _unused = 1;
	if (price === 0) {
		return true;
	}
	return false;
}
```

ولاحظ الخيار `--unsafe`، فتحويل `==` إلى `===` قد يغير سلوك الكود إذا كان أحدهم يعتمد على التحويل، لذلك لا يطبقه Biome إلا عندما تطلبه صراحة، أما `--write` وحده فيطبق الإصلاحات الآمنة فقط. ولاحظ أيضاً أن الملف صار بـ Tab وليس بمسافتين، وأن المتغير غير المستخدم لم يحذف وإنما أضيف قبله `_`، وهذا ما قادنا إلى المشكلة الأولى في الفقرة التالية.

### مشكلتان واجهناهما في التجربة Troubleshooting

المشكلة الأولى ظهرت عندما شغلنا Biome على المشروع كله بالأمر `npm run lint`، فرفض كل الملفات تقريباً، حتى `package.json` الذي أنشأه npm نفسه:

```text
package.json format
    10    │ - ····"format":·"biome·format·--write·."
       10 │ + → → "format":·"biome·format·--write·."
src/cli.ts:1:1 assist/source/organizeImports  FIXABLE
     1    │ - import·{·subtotal,·vat,·formatSar,·type·Item·}·from·'./price.ts';
        1 │ + import·{·formatSar,·type·Item,·subtotal,·vat·}·from·'./price.ts';
src/cli.ts format
     1    │ - import·{·subtotal,·vat,·formatSar,·type·Item·}·from·'./price.ts';
        1 │ + import·{·subtotal,·vat,·formatSar,·type·Item·}·from·"./price.ts";
```

والسبب أن Biome بدون ملف إعداد يستخدم Tab للمسافة البادئة وعلامات التنصيص المزدوجة، ويرتب أسماء `import` أبجدياً، بينما كتبنا الكود بمسافتين وعلامات مفردة كما في أغلب مشاريع JavaScript. والحل ليس أن تتجاهل الأداة، وإنما أن تقرر الأسلوب مرة واحدة وتكتبه في ملف، فأنشأنا الإعداد بالأمر `npx biome init`، وغيرنا فيه قيمتين:

```json
{
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "single"
    }
  }
}
```

ثم شغلنا `npx biome check --write .` فأصلح ثلاثة ملفات، وبقي خطأ واحد مقصود في `tour.ts` لأنه يستخدم `==` ليعرضه، وهذا ما يجب أن يحدث، فالقاعدة تمنعه في كل الكود ما لم تضع فوقه تعليق استثناء صريحاً بسببه. والدرس هنا أن قرار الأسلوب يتخذ في اليوم الأول ويكتب في ملف داخل Git، فلا يتحول كل Pull Request إلى نقاش عن المسافات.

والمشكلة الثانية أخطاء الوحدات، وهي أكثر ما يقابله القادم من Python أو Java في الأسبوع الأول. فقد كتبنا `import` بدون امتداد كما يفعل المحرر تلقائياً أحياناً:

```typescript
import { vat } from './price';
console.log(vat(2970));
```

وعند التشغيل:

```text
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/work/src/price' imported from /work/src/noext.ts
    at finalizeResolution (node:internal/modules/esm/resolve:241:11)
```

والسبب أن وحدات ES في Node.js لا تخمن الامتداد كما كان يفعل `require` القديم، فالمسار يجب أن يكون كاملاً، وهذا مذكور في توثيق TypeScript في Node.js صراحة. ثم جربنا العكس، أي `require` داخل ملف `.js` في مشروع ESM، لأن أغلب الأمثلة القديمة على الإنترنت تستخدمه:

```text
ReferenceError: require is not defined in ES module scope, you can use import instead
This file is being treated as an ES module because it has a '.js' file extension and '/work/package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
```

ورسالة Node.js هنا تشرح نفسها بالكامل: الملف يعامل كـ ES Module لأن امتداده `.js` والحقل `type` قيمته `module`، والحل إما `import` وإما أن تسمي الملف `.cjs`. وننصح بقراءة رسائل الخطأ في Node.js حتى آخر سطر، فهي في الإصدارات الحديثة تقول السبب والحل معاً في أغلب الحالات، وقد شرحنا طريقة قراءة رسائل الخطأ عموماً في مقال [قراءة رسالة الخطأ Stack Trace](https://arabroot.io/articles/%D9%82%D8%B1%D8%A7%D8%A1%D8%A9-%D8%B1%D8%B3%D8%A7%D9%84%D8%A9-%D8%A7%D9%84%D8%AE%D8%B7%D8%A7-stack-trace-%D9%88%D8%A7%D9%84%D8%AA%D8%B5%D8%AD%D9%8A%D8%AD-debugging/).

### ماذا يحدث إذا حذفت الحقل type من package.json؟

الآن نجرب ما يفعله كثيرون دون أن ينتبهوا، وهو مشروع بدون الحقل `type`، لأنه أنشئ بأداة قديمة أو نسخ من مشروع آخر:

```bash
npm pkg delete type
node src/cli.ts
```

والمخرج سوف يكون كما يلي:

```text
(node:337) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///work/src/cli.ts is not specified and it doesn't parse as CommonJS.
Reparsing as ES module because module syntax was detected. This incurs a performance overhead.
To eliminate this warning, add "type": "module" to /work/package.json.
(Use `node --trace-warnings ...` to show where the warning was created)
3 x هيل حب 100 غرام @ 9.90 ر.س
1 x تمر سكري 1 كيلو @ 38.00 ر.س
```

والملف عمل، ولكن بعد أن حاول Node.js قراءته كـ CommonJS أولاً، وهو النظام القديم الافتراضي، ففشل لأنه وجد `import`، فأعاد قراءته كـ ES Module، وطبع تحذيراً يقول صراحة إن هذا له تكلفة في الأداء. وهذه الميزة اسمها الكشف التلقائي عن الصيغة Syntax Detection، وجاءت لتجعل الملفات القديمة تعمل، ولكنها ليست شيئاً تعتمد عليه في مشروعك، فالتحذير يظهر في كل تشغيل وفي سجلات الإنتاج، وكل ملف يقرأ مرتين. والحل في نفس الرسالة: أضف `"type": "module"`.

### الخطوة الأخيرة: سكربتات npm Scripts في package.json

بعد أن عمل كل شيء نجمع الأوامر في قسم `scripts`، حتى يكتب كل من في الفريق نفس الأمر، ويستخدمه CI كما هو:

```json
{
  "name": "halala-cli",
  "version": "1.0.0",
  "description": "Prices in halalas: a first Node.js + TypeScript project",
  "scripts": {
    "start": "node src/cli.ts",
    "typecheck": "tsc -p .",
    "test": "node --test",
    "lint": "biome check .",
    "format": "biome format --write ."
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@biomejs/biome": "^2.6.4",
    "@types/node": "^26.6.6",
    "typescript": "^7.0.2"
  },
  "type": "module"
}
```

والأمر `npm run typecheck` يشغل `tsc` من `node_modules/.bin` المحلي، أي نفس الإصدار المثبت في ملف القفل، وليس أي `tsc` مثبت على جهازك عموماً، وهذا هو السبب الذي يجعلك تثبت الأدوات في المشروع نفسه وليس على مستوى النظام بالخيار `-g`. والسكربتان `test` و`start` لهما اختصار، فتكتب `npm test` و`npm start` دون كلمة `run`.

## هيكل المشروع Project Layout: ما يدخل Git وما لا يدخل

لا يفرض Node.js أي هيكل للمجلدات، وهذا يربك القادم من Java حيث يحدد Maven مكان كل ملف، ولكن العرف المنتشر بسيط، والصورة التالية تبين هيكل مشروع التجربة:

![شجرة مجلد halala-cli: package.json وpackage-lock.json وtsconfig.json وbiome.json وsrc فيه price.ts وcli.ts وtest فيه price.test.ts كلها تدخل Git، وnode_modules بحجم 119 MB وdist لا تدخل، وبجانبها شرح دور package.json وملف القفل وtsconfig.json](https://arabroot.io/content/images/2026/10/js-for-devs-04-layout.webp)

ملفات الإعداد في الجذر، والكود في src، والاختبارات في test

والقاعدة في الصورة: كل ما تكتبه بيدك أو يحتاجه غيرك ليعيد بناء المشروع يدخل Git، وكل ما يمكن توليده من جديد لا يدخل. فـ `node_modules` يولد من ملف القفل بالأمر `npm ci`، و`dist` يولد من الكود بالبناء، ولذلك يكون ملف `.gitignore` في أي مشروع Node.js سطرين على الأقل:

```text
node_modules/
dist/
```

وبعض الفرق تضع الاختبارات بجانب الملف نفسه، مثل `src/price.test.ts` بجانب `src/price.ts`، وهذا مقبول ويعمل مع `node --test` بنفس الطريقة، والمهم أن يكون الاختيار واحداً في كل المشروع. وإذا كبر المشروع إلى عدة حزم في مستودع واحد، مثل الواجهة والخدمة ومكتبة مشتركة للأنواع، فـ npm يدعم ذلك بخاصية Workspaces، وهذا موضوع لا تحتاجه في اليوم الأول.

وأضف حقلاً مفيداً إلى `package.json` اسمه `engines`، تكتب فيه إصدار Node.js الذي يعمل عليه المشروع، مثل `"node": ">=26"`، وملف `.nvmrc` فيه الرقم 26، فيعرف كل مطور جديد ومدير الإصدارات على جهازه الإصدار المطلوب دون أن يسأل أحداً.

## الوحدات Modules: ESM أم CommonJS؟

رأيت في التجربة ثلاث رسائل خطأ لها علاقة بالوحدات، والسبب تاريخي. فعندما ظهر Node.js في 2009 لم يكن للغة نظام وحدات، فاخترع Node.js نظامه الخاص CommonJS بالدالة `require` والكائن `module.exports`، وبنيت عليه مئات آلاف الحزم في npm. ثم أضاف المعيار في 2015 نظام الوحدات الرسمي ES Modules بالكلمتين `import` و`export`، وهو الذي تفهمه المتصفحات، وأخذ Node.js سنوات حتى دعمه بشكل كامل، وفي هذه السنوات عاش النظامان معاً بقواعد معقدة. والصورة التالية تلخص كيف يقرر Node.js 26 نوع الملف، والرسائل التي رأيناها:

![ثلاثة صناديق: مع type module تعامل ملفات js وts كـ ES Modules وملف cjs يبقى CommonJS، وما رأيناه عند الخطأ: ERR_MODULE_NOT_FOUND بدون امتداد وrequire is not defined في ESM، وبدون الحقل type يعيد Node.js قراءة الملف ويطبع تحذير MODULE_TYPELESS_PACKAGE_JSON، وتحتها قاعدة لمشروع جديد](https://arabroot.io/content/images/2026/10/js-for-devs-05-modules.webp)

الحقل type والامتداد يحددان نوع الوحدة، وليس محتوى الملف

والقواعد كما في [توثيق الحزم في Node.js](https://nodejs.org/docs/latest-v26.x/api/packages.html?ref=arabroot.io) هي: ملف `.mjs` أو `.mts` هو ES Module دائماً، وملف `.cjs` أو `.cts` هو CommonJS دائماً، أما `.js` و`.ts` فيحددهما الحقل `type` في أقرب `package.json`. والخبر الجيد للقادمين الجدد أن الفجوة بين النظامين صارت صغيرة، فمنذ الإصدارات 22.12 و20.19 يستطيع ملف CommonJS أن يستورد وحدة ES بالدالة `require` مباشرة ما دامت لا تستخدم `await` في أعلى الملف، كما يشرح [توثيق الوحدات](https://nodejs.org/docs/latest-v26.x/api/modules.html?ref=arabroot.io)، وصارت الميزة مستقرة في الإصدار 25.4\. وهذا أنهى أكبر مشكلة كانت تواجه المشاريع القديمة عندما تحولت مكتبات مشهورة إلى ESM فقط.

والقرار العملي لمشروع جديد سهل: `"type": "module"`، و`import` في كل مكان، والامتداد كاملاً في المسارات النسبية. وإذا ورثت مشروعاً قديماً بـ CommonJS فلا تحوله كله في يوم واحد، فالنظامان يعملان معاً الآن، وتستطيع تحويل الملفات تدريجياً، وابدأ بالملفات الجديدة.

## التشغيل في الإنتاج Production: ملفات .ts مباشرة أم البناء إلى JavaScript؟

بعد أن صار Node.js يشغل TypeScript مباشرة، ظهر سؤال جديد في كل فريق: هل نشغل ملفات `.ts` في الإنتاج أيضاً، أم نبني مجلد `dist` بـ JavaScript كما كنا نفعل؟ ولكي لا نجيب بالرأي قسنا زمن البدء Startup Time لأداة الهللات بأربع طرق، كل طريقة 20 مرة بالتناوب داخل نفس الـ Container، حتى يتوزع أثر ضجيج الجهاز على الطرق كلها بالتساوي. والبناء إلى `dist` تم بـ `tsc` نفسه مع الخيار `rewriteRelativeImportExtensions` الذي يحول `./price.ts` إلى `./price.js` في الملفات الخارجة:

```bash
npx tsc -p . --noEmit false --outDir dist --rewriteRelativeImportExtensions --allowImportingTsExtensions false
mkdir -p /tmp/ncc; NODE_COMPILE_CACHE=/tmp/ncc node src/cli.ts >/dev/null
a=0; b=0; c=0; d=0
for i in $(seq 1 20); do
  t0=$(date +%s%N); node -e 0
  t1=$(date +%s%N); node dist/src/cli.js >/dev/null
  t2=$(date +%s%N); node src/cli.ts >/dev/null
  t3=$(date +%s%N); NODE_COMPILE_CACHE=/tmp/ncc node src/cli.ts >/dev/null
  t4=$(date +%s%N)
  a=$((a+t1-t0)); b=$((b+t2-t1)); c=$((c+t3-t2)); d=$((d+t4-t3))
done
```

والمخرج سوف يكون كما يلي:

```text
node -e 0                    129 ms
node dist/src/cli.js         140 ms
node src/cli.ts              274 ms
node src/cli.ts + cache      143 ms
```

والصورة التالية تبين نفس الأرقام:

![أربعة أعمدة أفقية لزمن البدء: node -e 0 بـ 129 ms، وملف JavaScript المبني بـ 140 ms، وملف ts مباشرة بـ 274 ms، وملف ts مع NODE_COMPILE_CACHE بـ 143 ms، مع ملاحظتين: الحذف أضاف حوالي 134 ms، ومع الـ Compile Cache صار الفرق 3 ms](https://arabroot.io/content/images/2026/10/js-for-devs-06-startup.webp)

تكلفة حذف الأنواع تدفع عند كل تشغيل، إلا إذا فعلت الـ Compile Cache

وهذه الأرقام تقرأ كما يلي:

- Node.js فارغ يحتاج 129 ميلي ثانية للبدء داخل هذا الـ Container على Docker Desktop، وهذا رقم أعلى مما ستراه على Linux مباشرة، لذلك المهم هو الفرق بين الصفوف وليس الرقم نفسه.
- ملف JavaScript المبني أضاف 11 ميلي ثانية فقط فوق Node.js الفارغ، وهي قراءة ملفين وترجمتهما.
- ملف TypeScript مباشرة أضاف 145 ميلي ثانية، أي أن حذف الأنواع كلف 134 ميلي ثانية أكثر من JavaScript المبنية، لأن Node.js يحمل أداة الحذف ويمرر عليها كل ملف في كل تشغيل.
- مع متغير البيئة `NODE_COMPILE_CACHE` انخفض الفرق عن JavaScript المبنية إلى 3 ميلي ثانية، لأن [الـ Compile Cache](https://nodejs.org/docs/latest-v26.x/api/module.html?ref=arabroot.io) يحفظ نتيجة الترجمة على القرص، ويذكر التوثيق صراحة أنه يشمل وحدات TypeScript، فيدفع الثمن في أول تشغيل فقط.

وقد يتساءل البعض: هل 134 ميلي ثانية مهمة؟ والإجابة تعتمد على نوع البرنامج. فخدمة تعمل أياماً دون إعادة تشغيل تدفع هذا الثمن مرة واحدة عند البدء، فلا يهمها، أما أداة سطر أوامر تعمل مئات المرات في اليوم، أو دالة Serverless تبدأ من الصفر مع كل طلب بارد Cold Start، فالفرق محسوس. وفي مشروع كبير بمئات الملفات يكبر الرقم مع عدد الملفات. ولذلك فإن القرار العملي:

- التطوير والسكربتات الداخلية: شغل `.ts` مباشرة، فلا توجد خطوة بناء تنساها، ورسائل الخطأ تشير إلى ملفاتك كما كتبتها.
- خدمة طويلة العمر: كلا الطريقين مقبول، ومع Docker Image يمكنك أن تبني `dist` في مرحلة البناء، أو أن تشغل `.ts` مع `NODE_COMPILE_CACHE`.
- أداة توزعها على مستخدمين أو مكتبة تنشرها على npm: ابن إلى JavaScript دائماً، لأن Node.js يرفض حذف الأنواع من الملفات الموجودة داخل `node_modules`، فالمكتبة التي تنشر ملفات `.ts` فقط لن تعمل عند من يثبتها.

## أخطاء شائعة Common Mistakes في الأسبوع الأول

- **الاعتماد على أن Node.js يفحص الأنواع**: رأيت في التجربة فاتورة خاطئة عملت بلا اعتراض، فاجعل `tsc` خطوة إجبارية في CI.
- **تثبيت الأدوات عالمياً بالخيار `-g`**: كل مطور سيكون لديه إصدار مختلف، والصحيح أن تكون في `devDependencies` وتشغل من `npm run` أو `npx`.
- **`npm install` في CI وفي Dockerfile**: قد يغير ملف القفل ويثبت إصدارات لم يختبرها أحد، واستخدم `npm ci`.
- **Docker Image بالوسم `latest` أو بإصدار فردي**: `node:latest` قد يكون إصداراً فردياً لن يدخل LTS، وثبت الإصدار الكامل مثل `node:26.13.0-slim`.
- **نسخ أمثلة `require` القديمة إلى مشروع ESM**: سوف ترى `require is not defined`، وحولها إلى `import`.
- **حذف الامتداد من `import`**: يعمل في بعض أدوات التجميع ويفشل في Node.js، واكتبه دائماً، و`module: nodenext` في `tsconfig.json` يكشفه قبل التشغيل.
- **استخدام `||` للقيمة الافتراضية**: يستبدل الصفر والنص الفارغ أيضاً، واستخدم `??` عندما يكون الصفر قيمة صحيحة مثل المخزون والخصم.
- **ترك قرار التنسيق بلا ملف إعداد**: كل أداة لها افتراضات مختلفة، فاكتب `biome.json` في اليوم الأول.
- **المبالغ بالريال كأرقام عشرية**: احسب بالهللات كأعداد صحيحة، والسبب بالأرقام في مقال الأنواع من هذه المرحلة، وفي مقال [الأرقام العشرية Floating Point من الداخل](https://arabroot.io/articles/%D8%A7%D9%84%D8%A3%D8%B1%D9%82%D8%A7%D9%85-%D8%A7%D9%84%D8%B9%D8%B4%D8%B1%D9%8A%D8%A9-floating-point-%D9%85%D9%86-%D8%A7%D9%84%D8%AF%D8%A7%D8%AE%D9%84/).

## قائمة تحقق Checklist لمشروع Node.js جديد

| البند            | القيمة المقترحة                                | السبب                                     |
| ---------------- | ---------------------------------------------- | ----------------------------------------- |
| إصدار Node.js    | آخر Active LTS، أي 26 الآن، في engines و.nvmrc | دعم حتى 2029، وكل الفريق على نفس الإصدار  |
| نوع الوحدات      | "type": "module"                               | المعيار الرسمي، وبدون تحذير إعادة القراءة |
| TypeScript       | tsc --noEmit مع strict وerasableSyntaxOnly     | الفحص في CI، والتشغيل بالحذف              |
| ملف القفل        | package-lock.json في Git، وnpm ci في CI        | نفس الشجرة في كل مكان                     |
| التنسيق والتدقيق | Biome مع biome.json                            | أداة واحدة وقرار أسلوب مكتوب              |
| الاختبارات       | node --test مع node:assert/strict              | بدون حزم إضافية                           |
| الإنتاج          | بناء dist أو NODE\_COMPILE\_CACHE              | تجنب تكلفة الحذف عند كل بدء               |
| Docker Image     | node:26.13.0-slim بإصدار كامل                  | بناء قابل للتكرار                         |

## تدريب عملي

- أضف إلى `price.ts` دالة خصم بنسبة مئوية تقرب نصف الهللة إلى الأعلى، واكتب لها اختبارين، أحدهما بخصم صفر، وتأكد أن `??` وليس `||` هو ما يحمي الصفر.
- غير `module` في `tsconfig.json` إلى `esnext` مع `moduleResolution: bundler`، واحذف الامتداد من أحد سطور `import`، ولاحظ أن `tsc` يقبله بينما يرفضه Node.js، ثم أرجع الإعداد.
- أضف `enum` صغيراً لحالة الطلب، وشغل الملف بـ Node.js، ثم شغل `tsc`، واقرأ الرسالتين، واستبدل الـ `enum` بنوع اتحاد Union من النصوص.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- JavaScript في المتصفح وفي Node.js نفس اللغة بمعيار ECMAScript، والفرق في المكتبات حول المحرك، وNode.js هو V8 مع libuv ومكتبات قياسية.
- للإنتاج اختر آخر إصدار زوجي في Active LTS، وهو Node.js 26 منذ 28 أكتوبر 2026، وثبته بإصدار كامل في Docker.
- Node.js 26 يشغل TypeScript بحذف الأنواع دون أي فحص، فالملف الذي فيه سعر نصي أعطى فاتورة خاطئة بصمت، و`tsc` أمسكه برمز خروج 1.
- أداة لكل خطوة: `npm` مع ملف القفل و`npm ci`، و`tsc` للفحص، وBiome للتنسيق والتدقيق بملف إعداد مكتوب، و`node --test` للاختبارات.
- `"type": "module"` والامتداد الكامل في `import` يمنعان أغلب أخطاء الوحدات التي يقابلها القادمون الجدد.
- حذف الأنواع أضاف 134 ميلي ثانية لكل تشغيل في تجربتنا، والـ Compile Cache أنزلها إلى 3، فشغل `.ts` مباشرة في التطوير، وابن أو فعل الـ Cache في الإنتاج.

وفي مرحلة [JavaScript وTypeScript](https://arabroot.io/start/#stage-lang-js) نكمل من هنا: الأنواع والقيم والذاكرة والأخطاء، ثم التزامن بالوعود والعمال، ثم خدمة الطلبات الحقيقية بـ Fastify وPostgreSQL، ثم الأخطاء الشائعة وخطة القراءة من المصادر الرسمية:

[![](https://arabroot.io/content/images/2026/10/typescript-for-devs-cover-cover.webp)دليلTypeScript للمطورين: أي الأخطاء تكشفها الأنواع Types؟زرعنا تسعة أخطاء في كود سلة مشتريات وفحصناها بالمترجم tsc بثلاثة إعدادات، لنرى ما يكشفه الوضع الصارم وما يمر رغم كل الأنواع ولماذا تحتاج التحقق وقت التشغيل.](https://arabroot.io/articles/typescript-%D9%84%D9%84%D9%85%D8%B7%D9%88%D8%B1%D9%8A%D9%86-%D9%85%D8%A7-%D8%A7%D9%84%D8%B0%D9%8A-%D8%AA%D9%83%D8%B4%D9%81%D9%87-%D8%A7%D9%84%D8%A7%D9%86%D9%88%D8%A7%D8%B9/)[![](https://arabroot.io/content/images/2026/10/js-event-loop-cover-cover.webp)دليلحلقة الأحداث Event Loop: لماذا تتأخر النقرة في صفحتك؟كيف ترتب JavaScript المهام والوعود والرسم على خيط واحد، ولماذا تنتظر النقرة خلف مهمة طويلة، وكيف تقسم العمل لتبقى الصفحة سريعة.](https://arabroot.io/articles/%D8%AD%D9%84%D9%82%D8%A9-%D8%A7%D9%84%D8%A7%D8%AD%D8%AF%D8%A7%D8%AB-event-loop-%D9%81%D9%8A-javascript/)

وإذا كنت ما زلت تختار اللغة لمشروعك القادم، فابدأ من مقال [كيف تختار لغة البرمجة لمشروعك](https://arabroot.io/articles/%D9%83%D9%8A%D9%81-%D8%AA%D8%AE%D8%AA%D8%A7%D8%B1-%D9%84%D8%BA%D8%A9-%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D9%84%D9%85%D8%B4%D8%B1%D9%88%D8%B9%D9%83/)، وإذا كان فريقك يكتب بأكثر من لغة فاقرأ [البرمجة بعدة لغات Polyglot](https://arabroot.io/articles/%D8%A7%D9%84%D8%A8%D8%B1%D9%85%D8%AC%D8%A9-%D8%A8%D8%B9%D8%AF%D8%A9-%D9%84%D8%BA%D8%A7%D8%AA-polyglot-%D9%85%D8%AA%D9%89-%D8%AA%D9%81%D9%8A%D8%AF-%D9%88%D9%85%D8%AA%D9%89-%D8%AA%D8%B9%D9%82%D8%AF-%D9%85%D8%B4%D8%B1%D9%88%D8%B9%D9%83/).

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة على Node.js v26.13.0 وnpm 11.20.0 وTypeScript 7.0.2 وBiome 2.6.4 داخل Docker Image الرسمي node:26.13.0-slim.